@rubytech/create-realagent 1.0.825 → 1.0.828
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/package.json +1 -1
- package/payload/platform/lib/task-secrets/dist/index.d.ts +40 -0
- package/payload/platform/lib/task-secrets/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/task-secrets/dist/index.js +44 -0
- package/payload/platform/lib/task-secrets/dist/index.js.map +1 -0
- package/payload/platform/lib/task-secrets/src/__tests__/redact-secrets.test.ts +127 -0
- package/payload/platform/lib/task-secrets/src/index.ts +77 -0
- package/payload/platform/lib/task-secrets/tsconfig.json +9 -0
- package/payload/platform/lib/task-secrets/vitest.config.ts +9 -0
- package/payload/platform/neo4j/schema.cypher +34 -2
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +19 -13
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +13 -12
- package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +4 -4
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/stream-log-review/SKILL.md +6 -6
- package/payload/platform/plugins/admin/skills/unzip-attachment/references/safety.md +1 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +3 -3
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -4
- package/payload/platform/plugins/docs/references/cloudflare.md +2 -2
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +2 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -2
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +6 -41
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +51 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +19 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +139 -56
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +61 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +241 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +5 -5
- package/payload/platform/plugins/memory/references/schema-base.md +6 -3
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +6 -6
- package/payload/platform/plugins/tasks/PLUGIN.md +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +11 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +19 -2
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +17 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/whatsapp-import/PLUGIN.md +17 -15
- package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +313 -366
- package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +27 -60
- package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.d.ts +18 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.js +31 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/delta-cursor.js.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts +27 -12
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.d.ts.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js +40 -20
- package/payload/platform/plugins/whatsapp-import/lib/dist/derive-keys.js.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts +7 -4
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.d.ts.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.js +9 -6
- package/payload/platform/plugins/whatsapp-import/lib/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.d.ts +25 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.js +48 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/sessionize.js.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.d.ts +3 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.js +47 -0
- package/payload/platform/plugins/whatsapp-import/lib/dist/to-classifier-input.js.map +1 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/delta-append.test.ts +163 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/sessionize.test.ts +91 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/to-classifier-input.test.ts +59 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/delta-cursor.ts +54 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/derive-keys.ts +55 -32
- package/payload/platform/plugins/whatsapp-import/lib/src/index.ts +9 -6
- package/payload/platform/plugins/whatsapp-import/lib/src/sessionize.ts +81 -0
- package/payload/platform/plugins/whatsapp-import/lib/src/to-classifier-input.ts +48 -0
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +66 -73
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-archive-shape.md +143 -0
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +2 -2
- package/payload/platform/templates/specialists/agents/database-operator.md +17 -18
- package/payload/server/chunk-T2OPNP3L.js +654 -0
- package/payload/server/chunk-ZTBTX3IO.js +642 -0
- package/payload/server/cloudflare-task-tracker-BAMJY4MH.js +17 -0
- package/payload/server/cloudflare-task-tracker-CR6TL4VL.js +19 -0
- package/payload/server/public/assets/{admin-DOkUspG1.js → admin-BNwPsMhJ.js} +2 -2
- package/payload/server/public/assets/{graph-LLMJa4Ch.js → graph-N_Bw-8oT.js} +1 -1
- package/payload/server/public/assets/{page-DoaF3DB0.js → page-BKLGP-th.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +291 -172
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/filter-gate.test.ts +0 -172
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/ingest-idempotence.test.ts +0 -141
- package/payload/platform/plugins/whatsapp-import/lib/src/filter.ts +0 -136
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import-enrich/SKILL.md +0 -333
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
2
|
+
[key: string]: JsonValue;
|
|
3
|
+
};
|
|
4
|
+
export interface FormSchema {
|
|
5
|
+
/**
|
|
6
|
+
* Field names whose values are secret and must never land on a Task node.
|
|
7
|
+
* Declared at the form definition (e.g. `cloudflare-setup-form`'s schema in
|
|
8
|
+
* `cloudflare-setup-types.ts`), not at the writer. Writers pass the form
|
|
9
|
+
* schema through unmodified — the contract is "secrets are declared once
|
|
10
|
+
* where the form is defined, redaction is enforced once at write time."
|
|
11
|
+
*/
|
|
12
|
+
secretFields?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface RedactResult {
|
|
15
|
+
/**
|
|
16
|
+
* Payload with every `secretFields` entry removed. Non-secret keys keep
|
|
17
|
+
* their values intact (including null, false, 0, empty string). Keys not
|
|
18
|
+
* mentioned in `secretFields` always pass through — secrets are an
|
|
19
|
+
* allow-list of names to drop, not the entire universe.
|
|
20
|
+
*/
|
|
21
|
+
redacted: Record<string, JsonValue>;
|
|
22
|
+
/** Number of fields that were stripped. Callers log this when > 0. */
|
|
23
|
+
droppedFields: number;
|
|
24
|
+
/** Names of fields that were stripped, sorted for stable log lines. */
|
|
25
|
+
droppedFieldNames: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
|
|
29
|
+
* no logging, no thrown errors. Caller is responsible for the
|
|
30
|
+
* `[task] redacted ...` log line when `droppedFields > 0`.
|
|
31
|
+
*
|
|
32
|
+
* Edge-case behaviour:
|
|
33
|
+
* - `payload` undefined / null → empty result, droppedFields=0.
|
|
34
|
+
* - `schema` undefined / no `secretFields` → every key passes through.
|
|
35
|
+
* - A secret field whose key is absent from `payload` → no-op (not counted).
|
|
36
|
+
* - Non-string values on a secret field → key still dropped, value never
|
|
37
|
+
* inspected. Redaction is by KEY, not by value-shape heuristic.
|
|
38
|
+
*/
|
|
39
|
+
export declare function redactSecrets(payload: Record<string, JsonValue> | null | undefined, schema: FormSchema | null | undefined): RedactResult;
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,sEAAsE;IACtE,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EACrD,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACpC,YAAY,CAgBd"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Task 890 — outcome contract for audit Task input persistence.
|
|
3
|
+
//
|
|
4
|
+
// Doctrine: every audit Task records enough operator-meaningful information to
|
|
5
|
+
// explain what happened, AND no audit Task contains secret material. Mechanism
|
|
6
|
+
// is centralised here, not at the per-kind writer: form/action input
|
|
7
|
+
// definitions tag secret fields at the source of truth, and writers strip
|
|
8
|
+
// those fields via `redactSecrets` before persisting `inputs.<field>` props
|
|
9
|
+
// onto the Task node.
|
|
10
|
+
//
|
|
11
|
+
// Single file by design — no per-kind branching, no factory. A future
|
|
12
|
+
// schema shape change extends `FormSchema` here; the contract stays one place.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.redactSecrets = redactSecrets;
|
|
15
|
+
/**
|
|
16
|
+
* Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
|
|
17
|
+
* no logging, no thrown errors. Caller is responsible for the
|
|
18
|
+
* `[task] redacted ...` log line when `droppedFields > 0`.
|
|
19
|
+
*
|
|
20
|
+
* Edge-case behaviour:
|
|
21
|
+
* - `payload` undefined / null → empty result, droppedFields=0.
|
|
22
|
+
* - `schema` undefined / no `secretFields` → every key passes through.
|
|
23
|
+
* - A secret field whose key is absent from `payload` → no-op (not counted).
|
|
24
|
+
* - Non-string values on a secret field → key still dropped, value never
|
|
25
|
+
* inspected. Redaction is by KEY, not by value-shape heuristic.
|
|
26
|
+
*/
|
|
27
|
+
function redactSecrets(payload, schema) {
|
|
28
|
+
if (!payload) {
|
|
29
|
+
return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
|
|
30
|
+
}
|
|
31
|
+
const secrets = new Set(schema?.secretFields ?? []);
|
|
32
|
+
const redacted = {};
|
|
33
|
+
const dropped = [];
|
|
34
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
35
|
+
if (secrets.has(key)) {
|
|
36
|
+
dropped.push(key);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
redacted[key] = value;
|
|
40
|
+
}
|
|
41
|
+
dropped.sort();
|
|
42
|
+
return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,gEAAgE;AAChE,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,qEAAqE;AACrE,0EAA0E;AAC1E,4EAA4E;AAC5E,sBAAsB;AACtB,EAAE;AACF,sEAAsE;AACtE,+EAA+E;;AA+C/E,sCAmBC;AA/BD;;;;;;;;;;;GAWG;AACH,SAAgB,aAAa,CAC3B,OAAqD,EACrD,MAAqC;IAErC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAC;IACnE,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,QAAQ,GAA8B,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,IAAI,EAAE,CAAC;IACf,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Task 890 — no-secret-leak contract test.
|
|
2
|
+
//
|
|
3
|
+
// Verifies that `redactSecrets` strips every key declared as a secret in the
|
|
4
|
+
// form schema, and is meaningful (not a no-op) — removing the `secret` tag
|
|
5
|
+
// from a field MUST cause a deliberately-secret value to leak into the result,
|
|
6
|
+
// proving the test would catch a regression.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { redactSecrets, type FormSchema } from '../index.js';
|
|
10
|
+
|
|
11
|
+
const SECRET_VALUE = 'hunter2';
|
|
12
|
+
const SECRET_REGEX = /password|token|api[_-]?key|secret|credential|bearer/i;
|
|
13
|
+
|
|
14
|
+
describe('redactSecrets', () => {
|
|
15
|
+
it('drops every key listed in secretFields', () => {
|
|
16
|
+
const schema: FormSchema = { secretFields: ['password', 'apiKey'] };
|
|
17
|
+
const payload = {
|
|
18
|
+
adminLabel: 'maxy-test',
|
|
19
|
+
adminDomain: 'maxy.bot',
|
|
20
|
+
password: SECRET_VALUE,
|
|
21
|
+
apiKey: 'sk-abc',
|
|
22
|
+
};
|
|
23
|
+
const { redacted, droppedFields, droppedFieldNames } = redactSecrets(payload, schema);
|
|
24
|
+
|
|
25
|
+
expect(redacted).toEqual({ adminLabel: 'maxy-test', adminDomain: 'maxy.bot' });
|
|
26
|
+
expect(droppedFields).toBe(2);
|
|
27
|
+
expect(droppedFieldNames).toEqual(['apiKey', 'password']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('passes every key through when schema has no secretFields', () => {
|
|
31
|
+
const payload = { mode: 'personal', emailProvided: true };
|
|
32
|
+
const { redacted, droppedFields } = redactSecrets(payload, {});
|
|
33
|
+
expect(redacted).toEqual(payload);
|
|
34
|
+
expect(droppedFields).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('passes every key through when schema is undefined', () => {
|
|
38
|
+
const payload = { mode: 'personal' };
|
|
39
|
+
const { redacted, droppedFields } = redactSecrets(payload, undefined);
|
|
40
|
+
expect(redacted).toEqual(payload);
|
|
41
|
+
expect(droppedFields).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns empty result for null/undefined payload', () => {
|
|
45
|
+
expect(redactSecrets(null, { secretFields: ['x'] })).toEqual({
|
|
46
|
+
redacted: {},
|
|
47
|
+
droppedFields: 0,
|
|
48
|
+
droppedFieldNames: [],
|
|
49
|
+
});
|
|
50
|
+
expect(redactSecrets(undefined, undefined)).toEqual({
|
|
51
|
+
redacted: {},
|
|
52
|
+
droppedFields: 0,
|
|
53
|
+
droppedFieldNames: [],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('preserves falsy non-secret values (false, 0, empty string, null)', () => {
|
|
58
|
+
const payload = { a: false, b: 0, c: '', d: null, secret: 's' };
|
|
59
|
+
const { redacted } = redactSecrets(payload, { secretFields: ['secret'] });
|
|
60
|
+
expect(redacted).toEqual({ a: false, b: 0, c: '', d: null });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not count secret keys absent from payload', () => {
|
|
64
|
+
const schema: FormSchema = { secretFields: ['password', 'missingField'] };
|
|
65
|
+
const { droppedFields, droppedFieldNames } = redactSecrets({ password: SECRET_VALUE }, schema);
|
|
66
|
+
expect(droppedFields).toBe(1);
|
|
67
|
+
expect(droppedFieldNames).toEqual(['password']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('drops a secret key regardless of its value shape', () => {
|
|
71
|
+
const schema: FormSchema = { secretFields: ['password'] };
|
|
72
|
+
const cases: Array<{ password: unknown }> = [
|
|
73
|
+
{ password: 'string-value' },
|
|
74
|
+
{ password: 42 },
|
|
75
|
+
{ password: null },
|
|
76
|
+
{ password: { nested: 'object' } },
|
|
77
|
+
{ password: ['array', 'of', 'strings'] },
|
|
78
|
+
];
|
|
79
|
+
for (const c of cases) {
|
|
80
|
+
const { redacted } = redactSecrets(c as Record<string, never>, schema);
|
|
81
|
+
expect(redacted).not.toHaveProperty('password');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('CONTRACT: no recorded property value contains a secret regex match (cloudflare-setup-form)', () => {
|
|
86
|
+
// Synthetic cloudflare submission shape — mirrors CloudflareSetupRequest
|
|
87
|
+
// including both the operator-supplied secret (password) and the
|
|
88
|
+
// wire-protocol envelope (session_key, messageId) the schema also drops.
|
|
89
|
+
const cloudflareSchema: FormSchema = { secretFields: ['password', 'session_key', 'messageId'] };
|
|
90
|
+
const payload = {
|
|
91
|
+
adminLabel: 'maxy-test',
|
|
92
|
+
adminDomain: 'maxy.bot',
|
|
93
|
+
publicLabel: 'public',
|
|
94
|
+
publicDomain: 'maxy.bot',
|
|
95
|
+
apex: 'maxy.bot',
|
|
96
|
+
password: SECRET_VALUE,
|
|
97
|
+
tunnelName: 'maxy-test',
|
|
98
|
+
session_key: 'sk-conversation-correlation-12345678',
|
|
99
|
+
messageId: '11111111-2222-3333-4444-555555555555',
|
|
100
|
+
};
|
|
101
|
+
const { redacted } = redactSecrets(payload, cloudflareSchema);
|
|
102
|
+
|
|
103
|
+
// Scan every recorded value for any known-secret regex match.
|
|
104
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
105
|
+
const stringified = JSON.stringify(value);
|
|
106
|
+
expect(stringified, `field ${key} carried a secret-shaped value`).not.toMatch(SECRET_REGEX);
|
|
107
|
+
expect(stringified, `field ${key} carried the secret literal`).not.toContain(SECRET_VALUE);
|
|
108
|
+
}
|
|
109
|
+
// Wire-envelope fields must also be absent from recorded props.
|
|
110
|
+
expect(redacted).not.toHaveProperty('session_key');
|
|
111
|
+
expect(redacted).not.toHaveProperty('messageId');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('CONTRACT (falsifiability): removing the secret tag causes the password to leak — proves the test is meaningful', () => {
|
|
115
|
+
// This test is the canary that proves the no-secret-leak guarantee comes
|
|
116
|
+
// from the schema, not from coincidence. With `secretFields` empty (the
|
|
117
|
+
// schema-author forgot to tag `password`), the value MUST leak into the
|
|
118
|
+
// recorded payload — otherwise the contract test above would still pass
|
|
119
|
+
// even with a broken schema, and would catch nothing.
|
|
120
|
+
const brokenSchema: FormSchema = { secretFields: [] };
|
|
121
|
+
const payload = { adminLabel: 'maxy-test', password: SECRET_VALUE };
|
|
122
|
+
const { redacted } = redactSecrets(payload, brokenSchema);
|
|
123
|
+
|
|
124
|
+
expect(redacted.password).toBe(SECRET_VALUE);
|
|
125
|
+
expect(JSON.stringify(redacted)).toContain(SECRET_VALUE);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Task 890 — outcome contract for audit Task input persistence.
|
|
2
|
+
//
|
|
3
|
+
// Doctrine: every audit Task records enough operator-meaningful information to
|
|
4
|
+
// explain what happened, AND no audit Task contains secret material. Mechanism
|
|
5
|
+
// is centralised here, not at the per-kind writer: form/action input
|
|
6
|
+
// definitions tag secret fields at the source of truth, and writers strip
|
|
7
|
+
// those fields via `redactSecrets` before persisting `inputs.<field>` props
|
|
8
|
+
// onto the Task node.
|
|
9
|
+
//
|
|
10
|
+
// Single file by design — no per-kind branching, no factory. A future
|
|
11
|
+
// schema shape change extends `FormSchema` here; the contract stays one place.
|
|
12
|
+
|
|
13
|
+
export type JsonValue =
|
|
14
|
+
| string
|
|
15
|
+
| number
|
|
16
|
+
| boolean
|
|
17
|
+
| null
|
|
18
|
+
| JsonValue[]
|
|
19
|
+
| { [key: string]: JsonValue };
|
|
20
|
+
|
|
21
|
+
export interface FormSchema {
|
|
22
|
+
/**
|
|
23
|
+
* Field names whose values are secret and must never land on a Task node.
|
|
24
|
+
* Declared at the form definition (e.g. `cloudflare-setup-form`'s schema in
|
|
25
|
+
* `cloudflare-setup-types.ts`), not at the writer. Writers pass the form
|
|
26
|
+
* schema through unmodified — the contract is "secrets are declared once
|
|
27
|
+
* where the form is defined, redaction is enforced once at write time."
|
|
28
|
+
*/
|
|
29
|
+
secretFields?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RedactResult {
|
|
33
|
+
/**
|
|
34
|
+
* Payload with every `secretFields` entry removed. Non-secret keys keep
|
|
35
|
+
* their values intact (including null, false, 0, empty string). Keys not
|
|
36
|
+
* mentioned in `secretFields` always pass through — secrets are an
|
|
37
|
+
* allow-list of names to drop, not the entire universe.
|
|
38
|
+
*/
|
|
39
|
+
redacted: Record<string, JsonValue>;
|
|
40
|
+
/** Number of fields that were stripped. Callers log this when > 0. */
|
|
41
|
+
droppedFields: number;
|
|
42
|
+
/** Names of fields that were stripped, sorted for stable log lines. */
|
|
43
|
+
droppedFieldNames: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Strip every secret-tagged field from `payload` per `schema`. Pure: no IO,
|
|
48
|
+
* no logging, no thrown errors. Caller is responsible for the
|
|
49
|
+
* `[task] redacted ...` log line when `droppedFields > 0`.
|
|
50
|
+
*
|
|
51
|
+
* Edge-case behaviour:
|
|
52
|
+
* - `payload` undefined / null → empty result, droppedFields=0.
|
|
53
|
+
* - `schema` undefined / no `secretFields` → every key passes through.
|
|
54
|
+
* - A secret field whose key is absent from `payload` → no-op (not counted).
|
|
55
|
+
* - Non-string values on a secret field → key still dropped, value never
|
|
56
|
+
* inspected. Redaction is by KEY, not by value-shape heuristic.
|
|
57
|
+
*/
|
|
58
|
+
export function redactSecrets(
|
|
59
|
+
payload: Record<string, JsonValue> | null | undefined,
|
|
60
|
+
schema: FormSchema | null | undefined,
|
|
61
|
+
): RedactResult {
|
|
62
|
+
if (!payload) {
|
|
63
|
+
return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
|
|
64
|
+
}
|
|
65
|
+
const secrets = new Set(schema?.secretFields ?? []);
|
|
66
|
+
const redacted: Record<string, JsonValue> = {};
|
|
67
|
+
const dropped: string[] = [];
|
|
68
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
69
|
+
if (secrets.has(key)) {
|
|
70
|
+
dropped.push(key);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
redacted[key] = value;
|
|
74
|
+
}
|
|
75
|
+
dropped.sort();
|
|
76
|
+
return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
|
|
77
|
+
}
|
|
@@ -258,6 +258,38 @@ OPTIONS {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
+
// ----------------------------------------------------------
|
|
262
|
+
// ConversationArchive — chunked WhatsApp/messaging archive (Task 891).
|
|
263
|
+
//
|
|
264
|
+
// :ConversationArchive parent + :Section:Conversation chunks (HAS_SECTION + NEXT chain).
|
|
265
|
+
// MERGE-keyed on conversationIdentity = sha256(accountId + ":" + sortedParticipantElementIds).
|
|
266
|
+
// :Section base label is shared with KnowledgeDocument's children — chunks
|
|
267
|
+
// reuse the section_embedding vector index above and the universal fulltext
|
|
268
|
+
// index below.
|
|
269
|
+
//
|
|
270
|
+
// :PARTICIPANT_IN edges from :Person/:AdminUser to :ConversationArchive are
|
|
271
|
+
// MERGEd on every ingest (idempotent); the operator confirms each participant
|
|
272
|
+
// up front via the whatsapp-import skill flow.
|
|
273
|
+
// ----------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
CREATE CONSTRAINT conversation_archive_identity_unique IF NOT EXISTS
|
|
276
|
+
FOR (a:ConversationArchive) REQUIRE a.conversationIdentity IS UNIQUE;
|
|
277
|
+
|
|
278
|
+
CREATE INDEX conversation_archive_account IF NOT EXISTS
|
|
279
|
+
FOR (a:ConversationArchive) ON (a.accountId);
|
|
280
|
+
|
|
281
|
+
CREATE INDEX conversation_archive_session IF NOT EXISTS
|
|
282
|
+
FOR (a:ConversationArchive) ON (a.createdBySession);
|
|
283
|
+
|
|
284
|
+
CREATE VECTOR INDEX conversation_archive_embedding IF NOT EXISTS
|
|
285
|
+
FOR (a:ConversationArchive) ON (a.embedding)
|
|
286
|
+
OPTIONS {
|
|
287
|
+
indexConfig: {
|
|
288
|
+
`vector.dimensions`: 768,
|
|
289
|
+
`vector.similarity_function`: 'cosine'
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
261
293
|
// Universal full-text BM25 index for hybrid keyword search (Task 748).
|
|
262
294
|
//
|
|
263
295
|
// Every operator-meaningful label written by the platform is in the index union;
|
|
@@ -276,7 +308,7 @@ OPTIONS {
|
|
|
276
308
|
// Label union — every operator-meaningful label:
|
|
277
309
|
// - Business identity: LocalBusiness, Service, PriceSpecification, OpeningHoursSpecification, Organization
|
|
278
310
|
// - People: Person, UserProfile, Preference, AdminUser, AccessGrant
|
|
279
|
-
// - Knowledge: KnowledgeDocument, Section, Chunk (legacy), DigitalDocument, CreativeWork,
|
|
311
|
+
// - Knowledge: KnowledgeDocument, ConversationArchive (Task 891), Section, Chunk (legacy), DigitalDocument, CreativeWork,
|
|
280
312
|
// Question, FAQPage, DefinedTerm, Review, ImageObject
|
|
281
313
|
// - Conversational: Conversation, AdminConversation, PublicConversation, Message,
|
|
282
314
|
// UserMessage, AssistantMessage, ToolCall
|
|
@@ -309,7 +341,7 @@ OPTIONS {
|
|
|
309
341
|
CREATE FULLTEXT INDEX entity_search IF NOT EXISTS
|
|
310
342
|
FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
|
|
311
343
|
|Person|UserProfile|Preference|AdminUser|AccessGrant
|
|
312
|
-
|KnowledgeDocument|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
|
|
344
|
+
|KnowledgeDocument|ConversationArchive|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
|
|
313
345
|
|Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
|
|
314
346
|
|Task|Project|Event
|
|
315
347
|
|Workflow|WorkflowStep|WorkflowRun|StepResult
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p plugins/whatsapp-import/lib/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Archive-ingest surface gate (Task 855
|
|
2
|
+
# Archive-ingest surface gate (Task 855, updated by Task 891).
|
|
3
3
|
#
|
|
4
4
|
# Five enforcements, one script — phase decided by `hook_event_name` on stdin.
|
|
5
5
|
# Task 855 narrows the database-operator subagent's effective surface during
|
|
6
6
|
# WhatsApp archive ingestion to exactly one Bash entry
|
|
7
7
|
# (`whatsapp-import/bin/whatsapp-ingest.sh`) plus read-only neighbours, by
|
|
8
|
-
# blocking the legacy MCP deviation tools mechanically.
|
|
8
|
+
# blocking the legacy MCP deviation tools mechanically. Task 891 retired the
|
|
9
|
+
# `whatsapp-export-insight-pass` tool entirely (Phase 2 enrichment moved to a
|
|
10
|
+
# separate follow-up task that will operate on :Section:Conversation chunks);
|
|
11
|
+
# the tool name is added to the BLOCK list so any agent that still references
|
|
12
|
+
# it from a stale skill or runbook gets a loud denial instead of MCP-not-found.
|
|
9
13
|
#
|
|
10
|
-
# 1. PreToolUse on the
|
|
11
|
-
# The single deterministic Bash entry
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
+
# 1. PreToolUse on the four legacy WhatsApp MCP tools — BLOCK unconditionally.
|
|
15
|
+
# The single deterministic Bash entry is the only supported path for
|
|
16
|
+
# `archiveType=whatsapp-export`. Tool source for #1-#3 remains until cleanup;
|
|
17
|
+
# `whatsapp-export-insight-pass` source was deleted by Task 891 and the
|
|
18
|
+
# block here catches stale references.
|
|
14
19
|
# mcp__memory__whatsapp-export-parse
|
|
15
20
|
# mcp__memory__whatsapp-export-insight-write
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
21
|
+
# mcp__memory__whatsapp-export-insight-pass (Task 891 — deleted, retired)
|
|
22
|
+
# mcp__memory__memory-archive-write (only when `archiveType` is
|
|
23
|
+
# `whatsapp-export`; LinkedIn
|
|
24
|
+
# and other archiveTypes pass
|
|
25
|
+
# through unchanged.)
|
|
20
26
|
#
|
|
21
27
|
# 2. PreToolUse Edit/Write/NotebookEdit: deny writes under
|
|
22
28
|
# `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
|
|
@@ -136,9 +142,9 @@ fi
|
|
|
136
142
|
|
|
137
143
|
# --- Block 2: legacy WhatsApp MCP tools — denied unconditionally ----------
|
|
138
144
|
case "$TOOL_NAME" in
|
|
139
|
-
mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write)
|
|
145
|
+
mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write|mcp__memory__whatsapp-export-insight-pass)
|
|
140
146
|
emit_decision "block" "denied-mcp-legacy" \
|
|
141
|
-
"Blocked: ${TOOL_NAME} is
|
|
147
|
+
"Blocked: ${TOOL_NAME} is a retired path. Task 891 ships the chunked-archive contract — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Parse, sessionize, classify (mode=chat), and memory-ingest (parentLabel=ConversationArchive) all run in-process; do not call legacy MCP tools. Phase 2 insight derivation is deferred to its own follow-up task."
|
|
142
148
|
;;
|
|
143
149
|
esac
|
|
144
150
|
|
|
@@ -168,7 +174,7 @@ if [ "$TOOL_NAME" = "mcp__memory__memory-archive-write" ]; then
|
|
|
168
174
|
ARCHIVE_TYPE=$(extract_tool_input_field archiveType)
|
|
169
175
|
if [ "$ARCHIVE_TYPE" = "whatsapp-export" ]; then
|
|
170
176
|
emit_decision "block" "denied-mcp-legacy archiveType=whatsapp-export" \
|
|
171
|
-
"Blocked: memory-archive-write with archiveType='whatsapp-export' is
|
|
177
|
+
"Blocked: memory-archive-write with archiveType='whatsapp-export' is a retired path. Task 891 ships the chunked-archive contract — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Other archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
|
|
172
178
|
fi
|
|
173
179
|
fi
|
|
174
180
|
|
|
@@ -10,14 +10,14 @@ Applies when the business owner wants to set up, complete, or update their busin
|
|
|
10
10
|
## When to Activate
|
|
11
11
|
|
|
12
12
|
- Admin asks to set up, edit, or complete their business profile
|
|
13
|
-
- Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode
|
|
13
|
+
- Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode**. Personal mode bootstraps a `Person` with `role: "admin-personal"` directly inside step 9 and does NOT invoke this skill.
|
|
14
14
|
- Session-start graph check shows a `LocalBusiness` node with missing data (no hours, no services, empty description)
|
|
15
15
|
- The graph-write gate rejects a `memory-write` or `memory-update` with `no-admin-user` or `no-local-business` — the error message directs the agent here. (For personal-mode accounts, the gate is satisfied by the personal-profile `Person` node, so this rejection should not fire — if it does, the personal-profile node is missing or its `role` is wrong.)
|
|
16
16
|
- Admin asks questions like "update my address", "add our opening hours", "change the phone number"
|
|
17
17
|
|
|
18
18
|
## First-run path (no LocalBusiness yet)
|
|
19
19
|
|
|
20
|
-
The `AdminUser` and the operator's personal-profile `Person`
|
|
20
|
+
The `AdminUser` and the operator's personal-profile `Person` are written deterministically at PIN setup time, so this skill no longer creates the AdminUser. When onboarding step 9 invokes this skill in `business-owner` mode, or when the graph-write gate reports `no-local-business`, create the LocalBusiness:
|
|
21
21
|
|
|
22
22
|
1. **LocalBusiness.** Ask the user for the business name first — this is the only mandatory property. Call `memory-write` with `labels: ["LocalBusiness"]`, minimal `properties: { name }`, `scope: "shared"`, `relationships: [{ type: "OWNS", direction: "incoming", targetNodeId: "<AdminUser-elementId>" }]`. The `LocalBusiness` label is on the exempt set, so the gate passes for this write. Find the AdminUser elementId via `memory-search` for the operator's name; the AdminUser already exists from PIN setup.
|
|
23
23
|
2. **Capture the remaining domains** (address, hours, services, FAQs, brand assets) via `memory-update` on the `LocalBusiness` node and `memory-write` for related nodes (`OpeningHoursSpecification`, `Service`, `FAQPage`, `ImageObject`). With LocalBusiness present, the gate passes for all subsequent writes.
|
|
@@ -203,24 +203,25 @@ Pin the operator's persona and bootstrap the graph nodes that satisfy the graph-
|
|
|
203
203
|
|
|
204
204
|
**Call `onboarding-step9-mode` with the chosen mode before any graph write or skill invocation.** The tool emits the diagnostic log line and returns the deterministic next-action prose.
|
|
205
205
|
|
|
206
|
-
**Open the action record with `task-create` (
|
|
206
|
+
**Open the action record with `task-create` (process-provenance + audit input contract).** Before any graph write or skill invocation, call `task-create` with:
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```
|
|
208
|
+
- `name`: "Establish operator owner — onboarding step 9"
|
|
209
|
+
- `description`: a one-line summary describing the chosen mode and what entities will be produced
|
|
210
|
+
- `status`: `"running"`
|
|
211
|
+
- `kind`: `"onboarding-establish-owner"`
|
|
212
|
+
- `inputsProvided`: the keys you actually pass on `inputs` (e.g. `["mode"]`); records the call shape
|
|
213
|
+
- `inputs`: the form payload known at creation time — `{ mode: "personal" | "business-owner" }`
|
|
214
|
+
- `inputSchema`: `{ secretFields: [] }` — the step-9 mode select carries no secrets; declare the empty list explicitly so the contract is visible at the call site
|
|
216
215
|
|
|
217
216
|
The returned `taskId` is the action-provenance handle for this step — every subsequent `memory-write` for an action-provenance-gated label (`Person`, `UserProfile`, `AdminUser`, `Organization`, `LocalBusiness`) MUST pass it as `producedByTaskId` so the inbound `:PRODUCED` edge from the Task is composed into the write. The Task is auto-linked to the current `AdminConversation` via `RAISED_DURING` (this is what makes `MATCH (c:AdminConversation)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(entity)` traversable from the conversation that initiated onboarding).
|
|
218
217
|
|
|
218
|
+
**Recording what you collect.** The `onboarding-establish-owner` Task is the audit record for step 9. Record every operator-meaningful fact you collect using the existing surfaces — `description` for the running summary, `note` (via `task-update`) for facts as they land (email collected, phone declined, resolved Person/UserProfile elementIds), `appendStep` for phase markers. Agent's judgement decides which slot. Contract: by `task-complete`, a reader of the Task properties can reconstruct what was collected, what was declined, and which entities were touched without consulting any other source. No secrets in any property regardless of slot — `task-create`'s `inputs` is centrally redacted via `inputSchema.secretFields`; the post-creation slots carry no schema, so the agent's responsibility is to never write secret material into them.
|
|
219
|
+
|
|
219
220
|
Then branch on the mode.
|
|
220
221
|
|
|
221
222
|
### `business-owner`
|
|
222
223
|
|
|
223
|
-
Invoke the `business-profile` skill, passing the `taskId` so the skill can thread it as `producedByTaskId` into every `memory-write` it issues. The skill follows its first-run path: create the `AdminUser` node, create the `LocalBusiness` node, create the `Organization` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. When `business-profile` reports that the required nodes exist in the graph, call `task-update
|
|
224
|
+
Invoke the `business-profile` skill, passing the `taskId` so the skill can thread it as `producedByTaskId` into every `memory-write` it issues. The skill follows its first-run path: create the `AdminUser` node, create the `LocalBusiness` node, create the `Organization` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. When `business-profile` reports that the required nodes exist in the graph, call `task-update` with both `appendStep:"business-profile-complete"` AND `note:"Business profile complete — AdminUser=<elementId>, LocalBusiness=<elementId>, Organization=<elementId>"` (the resolved elementIds from the skill's writes; one call carries both the phase marker and the audit content). Then write the `HAS_PROFILE` edge from the personal-profile `Person` to the operator's `UserProfile` via `memory-update` (one `memory-search` to resolve both elementIds; the `UserProfile` already exists from step 6 onwards via the lazy-create in `loadUserProfile`). Then call `task-complete(taskId)` and `onboarding-complete-step` with step 9. Do not mark step 9 complete before the required nodes + the HAS_PROFILE edge exist — the gate's precondition must be real, not just recorded.
|
|
224
225
|
|
|
225
226
|
### `personal`
|
|
226
227
|
|
|
@@ -228,9 +229,9 @@ Personal mode does not register a `LocalBusiness`. The `AdminUser` and personal-
|
|
|
228
229
|
|
|
229
230
|
1. **Ask the user for their email AND phone number** in one short conversational message — {{productName}} stores both on the personal-profile Person for downstream features (notifications, contact-method matching, identity-coverage signal that the agent uses to detect missing identity in future turns). The user may decline either; record what they provide. (Operator-identity fix: the system-prompt's "Identity coverage" block surfaces missing identity fields on every turn, so a partial answer becomes a follow-up prompt rather than a silent gap.)
|
|
230
231
|
2. **Attach the identity to Person.** Call `profile-update` with `personFields: { email: "<value>", telephone: "<value>" }` (omit either key the user declined). The tool resolves the personal-profile Person via `(au:AdminUser {userId:$you})-[:OWNS]->(p:Person)` server-side and writes the canonical `email`/`telephone` fields. Use canonical `telephone` — `phone` is the schema synonym, not the canonical name; `profile-update` rejects `phone` rather than silently rewriting it. The tool throws if the OWNS edge is missing rather than silently no-oping (the PIN-setup path is the only place that edge is created — a missing edge means PIN setup never ran or was rolled back).
|
|
231
|
-
3. **Append the step.** Call `task-update
|
|
232
|
+
3. **Append the step + record the facts.** Call `task-update` with both `appendStep:"identity-attached"` AND `note:"Identity attached — email=<value-or-declined>, telephone=<value-or-declined>"` (use the actual values the operator supplied; for declines write the literal `declined`). One call, both fields. The note carries the operator-meaningful audit content; the step is the phase marker.
|
|
232
233
|
4. **Link the personal-profile `Person` to the `UserProfile`.** Call `memory-search` to resolve the `UserProfile` elementId for the operator (the lazy `loadUserProfile` write created it on the first admin session). Then call `memory-update` on the Person to add the `HAS_PROFILE` edge to the UserProfile. (`HAS_PROFILE` from `:Person` is a sibling pattern to the existing `AdminUser→HAS_PROFILE→UserProfile`; both are valid sources for the same edge type. See [schema-base.md Relationship Patterns](../../../memory/references/schema-base.md).)
|
|
233
|
-
5. **Close the action record.** Call `task-update
|
|
234
|
+
5. **Close the action record.** Call `task-update` with both `appendStep:"profile-linked"` AND `note:"Profile linked — Person=<elementId>, UserProfile=<elementId>"` (the resolved elementIds from steps 2 and 4). Then call `task-complete(taskId)`.
|
|
234
235
|
6. **Mark step 9 complete.** Call `onboarding-complete-step` with step 9.
|
|
235
236
|
|
|
236
237
|
After step 9 completes in personal mode, tell the user that {{productName}} is configured for personal use — their employer (if any) is not registered here. If they later become the operator for a business of their own, they can ask {{productName}} to set up a business profile, which invokes the `business-profile` skill directly.
|
|
@@ -22,7 +22,7 @@ Built-in plugins under `$PLATFORM_ROOT/plugins/`:
|
|
|
22
22
|
|
|
23
23
|
- Enable: call `plugin-toggle-enabled` with `pluginName` and `action: "enable"`. Activates the plugin's behaviour embed and MCP server from the next session. Plugins with scheduled behaviour (declared via `lifecycle` in PLUGIN.md frontmatter) are activated automatically by the platform heartbeat within one minute — no separate setup command needed.
|
|
24
24
|
- Disable: call `plugin-toggle-enabled` with `pluginName` and `action: "disable"`. Deactivates from the next session. Any scheduled Events owned by the plugin (`sourcePlugin` field) are cancelled automatically by the platform heartbeat.
|
|
25
|
-
- The tool refuses core plugins (admin, memory, docs, cloudflare, anthropic) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is denied by the pre-tool-use hook
|
|
25
|
+
- The tool refuses core plugins (admin, memory, docs, cloudflare, anthropic) and refuses plugins not installed under `$PLATFORM_ROOT/plugins/`. Direct `Edit` of `account.json` is denied by the pre-tool-use hook — `plugin-toggle-enabled` is the only legitimate path.
|
|
26
26
|
|
|
27
27
|
## Premium Plugins
|
|
28
28
|
|
|
@@ -38,11 +38,11 @@ When the user asks about a specific premium plugin (e.g., "tell me about the tea
|
|
|
38
38
|
|
|
39
39
|
### Recording a purchase
|
|
40
40
|
|
|
41
|
-
`purchasedPlugins` is an entitlement-bearing field
|
|
41
|
+
`purchasedPlugins` is an entitlement-bearing field: the effective list derives from the Rubytech-signed entitlement payload, not from raw `account.json`. The pre-tool-use hook denies any agent write to `account.json`. Direct purchase recording is therefore unavailable to the agent.
|
|
42
42
|
|
|
43
|
-
- **Production path:**
|
|
43
|
+
- **Production path:** the Rubytech-side issuance endpoint signs an updated entitlement payload after a Stripe purchase event and delivers it to `~/<configDir>/entitlement.json`. The verifier picks up the new payload at the next session start.
|
|
44
44
|
- **Pre-launch / dev path:** the founder runs `node platform/scripts/generate-entitlement-fixture.mjs sign --account-id <id> --email <e> --tier <t> --plugins a,b,c --out ~/<configDir>/entitlement.json` to produce a signed test payload. The agent does not invoke this script.
|
|
45
|
-
- **Personal-mode installs (`brand.json.commercialMode: false`, the default today):** purchasedPlugins is read from raw `account.json` via implicit-trust mode. Pre-launch the agent has no way to add purchases without operator intervention; this is intentional during the gap before
|
|
45
|
+
- **Personal-mode installs (`brand.json.commercialMode: false`, the default today):** purchasedPlugins is read from raw `account.json` via implicit-trust mode. Pre-launch the agent has no way to add purchases without operator intervention; this is intentional during the gap before signed-issuance ships.
|
|
46
46
|
|
|
47
47
|
If the user asks the agent to record a purchase, explain that their purchase is processed by Rubytech billing and the entitlement payload arrives on their device automatically — no agent action is required.
|
|
48
48
|
|
|
@@ -195,7 +195,7 @@ After creation, no template metadata persists in the agent's files. The resultin
|
|
|
195
195
|
9. **KNOWLEDGE.md generation** — populate from the now-tagged set plus keyword matches using the `update-knowledge` skill workflow
|
|
196
196
|
10. Write `config.json` with selected model, plugins, status "active", `liveMemory`, and `knowledgeKeywords`. This is the last gated write — placed after IDENTITY.md, SOUL.md, and KNOWLEDGE.md to prevent cascade failure if one gate stalls.
|
|
197
197
|
11. Check context budget — auto-summarise if over threshold
|
|
198
|
-
12. **Project the agent into the graph** — delegate to the `database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project
|
|
198
|
+
12. **Project the agent into the graph** — delegate to the `database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project`." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE.
|
|
199
199
|
13. Confirm creation: "Agent created. Visitors can reach it at `/{slug}`"
|
|
200
200
|
|
|
201
201
|
### List
|
|
@@ -245,7 +245,7 @@ Deletion removes the entire agent directory (`agents/{slug}/`) — not individua
|
|
|
245
245
|
- If no other agents remain (or the user declines to set a new default), clear the `defaultAgent` field by calling `account-update` with `field: "defaultAgent"`, `value: ""`. Tell the user that visitors to the root URL will see a "no agents" message until a new agent is created.
|
|
246
246
|
|
|
247
247
|
**Cleanup sequence:**
|
|
248
|
-
- Issue `DELETE /api/admin/agents/{slug}` — the route
|
|
248
|
+
- Issue `DELETE /api/admin/agents/{slug}` — the route removes the `:Agent` node and its four owned `:KnowledgeDocument` projections FIRST, then deletes the directory. Loud-fail: if graph cleanup throws, files are preserved and a 500 is returned with the error text. Re-issuing the DELETE is safe (idempotent on both layers). Operator-tagged docs survive the projection delete — only the `USES_KNOWLEDGE` edges from this Agent are removed.
|
|
249
249
|
- Verify the directory no longer exists
|
|
250
250
|
- Report what was removed (list the files that were in the directory)
|
|
251
251
|
|