@haaaiawd/second-nature 0.1.25 → 0.1.26
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/index.js +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/goal.d.ts +2 -0
- package/runtime/cli/commands/goal.js +5 -1
- package/runtime/cli/commands/index.js +1 -1
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.js +13 -5
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
- package/runtime/cli/read-models/index.js +81 -10
- package/runtime/cli/read-models/types.d.ts +10 -3
- package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
- package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +8 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +45 -4
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
- package/runtime/core/second-nature/index.d.ts +1 -0
- package/runtime/core/second-nature/index.js +1 -0
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +14 -2
- package/runtime/core/second-nature/orchestrator/goal-priority.js +2 -2
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
- package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
- package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
- package/runtime/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +73 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function kindToCapability(kind) {
|
|
2
|
+
if (kind === "exploration")
|
|
3
|
+
return "feed.read";
|
|
4
|
+
if (kind === "social")
|
|
5
|
+
return "comment.reply";
|
|
6
|
+
if (kind === "work")
|
|
7
|
+
return "work.discover";
|
|
8
|
+
if (kind === "outreach")
|
|
9
|
+
return "message.send";
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function getPlatformIds(registry) {
|
|
13
|
+
if (registry) {
|
|
14
|
+
return registry.listRegisteredPlatformIds();
|
|
15
|
+
}
|
|
16
|
+
// Fallback: built-in platforms when registry is absent (backward compat)
|
|
17
|
+
return ["moltbook", "instreet", "evomap"];
|
|
18
|
+
}
|
|
19
|
+
function extractPlatformIdsFromGoals(goals, kind, platformIds) {
|
|
20
|
+
const capability = kindToCapability(kind);
|
|
21
|
+
const results = new Set();
|
|
22
|
+
for (const goal of goals) {
|
|
23
|
+
const text = `${goal.description} ${goal.completionCriteria ?? ""}`.toLowerCase();
|
|
24
|
+
for (const pid of platformIds) {
|
|
25
|
+
if (text.includes(pid)) {
|
|
26
|
+
results.add(pid);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Also match if goal text contains the capability name (e.g. "feed.read")
|
|
30
|
+
if (capability && text.includes(capability.toLowerCase())) {
|
|
31
|
+
// capability alone doesn't tell us platform; keep for later
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...results];
|
|
35
|
+
}
|
|
36
|
+
function extractPlatformIdsFromEvidence(refs, platformIds) {
|
|
37
|
+
const results = new Set();
|
|
38
|
+
for (const ref of refs) {
|
|
39
|
+
if (ref.kind === "connector_result" && ref.id) {
|
|
40
|
+
for (const pid of platformIds) {
|
|
41
|
+
if (ref.id.includes(pid)) {
|
|
42
|
+
results.add(pid);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Parse platform:// URIs (e.g. platform://moltbook/feed.read)
|
|
47
|
+
if (ref.uri && ref.uri.startsWith("platform://")) {
|
|
48
|
+
const afterScheme = ref.uri.slice("platform://".length);
|
|
49
|
+
const platformPart = afterScheme.split("/")[0];
|
|
50
|
+
if (platformPart && platformIds.includes(platformPart)) {
|
|
51
|
+
results.add(platformPart);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// L-02: Also support namespace format moltbook:feed.read (connector-system §5.3)
|
|
55
|
+
if (ref.uri && !ref.uri.includes("://") && ref.uri.includes(":")) {
|
|
56
|
+
const nsPart = ref.uri.split(":")[0];
|
|
57
|
+
if (nsPart && platformIds.includes(nsPart)) {
|
|
58
|
+
results.add(nsPart);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...results];
|
|
63
|
+
}
|
|
64
|
+
function validatePlatformCapability(platformId, kind, registry) {
|
|
65
|
+
const capability = kindToCapability(kind);
|
|
66
|
+
if (!capability)
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
return registry.hasCapability(platformId, capability);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// H-08: Log registry validation failures for observability.
|
|
73
|
+
console.warn(`[platform-capability-router] Registry validation failed for ${platformId}:${capability}`, err);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve an explicit platformId for a candidate intent kind.
|
|
79
|
+
* Returns `undefined` when no unambiguous platform can be inferred.
|
|
80
|
+
*/
|
|
81
|
+
export function resolvePlatformForIntent(kind, context, registry) {
|
|
82
|
+
const capability = kindToCapability(kind);
|
|
83
|
+
if (!capability) {
|
|
84
|
+
// Quiet, reflection, maintenance have no connector capability mapping.
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const platformIds = getPlatformIds(registry);
|
|
88
|
+
const candidates = [];
|
|
89
|
+
if (context.acceptedGoals && context.acceptedGoals.length > 0) {
|
|
90
|
+
candidates.push(...extractPlatformIdsFromGoals(context.acceptedGoals, kind, platformIds));
|
|
91
|
+
}
|
|
92
|
+
if (context.evidenceRefs && context.evidenceRefs.length > 0) {
|
|
93
|
+
candidates.push(...extractPlatformIdsFromEvidence(context.evidenceRefs, platformIds));
|
|
94
|
+
}
|
|
95
|
+
// Deduplicate while preserving order
|
|
96
|
+
const ordered = [...new Set(candidates)];
|
|
97
|
+
if (ordered.length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (ordered.length > 1) {
|
|
101
|
+
// Ambiguous: multiple platforms inferred → do not guess, return undefined.
|
|
102
|
+
// Guard layer will deny with "ambiguous_platform" reason.
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const single = ordered[0];
|
|
106
|
+
if (registry) {
|
|
107
|
+
if (validatePlatformCapability(single, kind, registry)) {
|
|
108
|
+
return single;
|
|
109
|
+
}
|
|
110
|
+
// Registry says unsupported → undefined (guard layer will deny)
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
// No registry: best-effort return the single candidate (backward compat)
|
|
114
|
+
return single;
|
|
115
|
+
}
|
|
@@ -60,6 +60,15 @@ function eventMatchesQuery(envelope, query) {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
case "relationship": {
|
|
64
|
+
const needle = query.relationshipId;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(payload).includes(needle);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
function summarizeEnvelope(e) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CredentialState = "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
|
|
1
|
+
export type CredentialState = "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
|
|
2
2
|
export type CredentialType = "api_key" | "oauth_token" | "node_secret" | "verification_code";
|
|
3
3
|
export interface CredentialContext {
|
|
4
4
|
platformId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StateDatabase } from "../db/index.js";
|
|
2
|
-
export type ChronicleEventKind = "heartbeat" | "connector_action" | "outreach" | "owner_reply" | "dream_run" | "maintenance";
|
|
2
|
+
export type ChronicleEventKind = "heartbeat" | "connector_action" | "outreach" | "owner_reply" | "dream_run" | "maintenance" | "system_notice";
|
|
3
3
|
export interface SourceRef {
|
|
4
4
|
sourceId: string;
|
|
5
5
|
kind: string;
|
|
@@ -10,4 +10,22 @@ export interface CredentialVault {
|
|
|
10
10
|
loadCredentialContext(platformId: string): Promise<CredentialContext | null>;
|
|
11
11
|
getCredentialState(platformId: string): Promise<CredentialState>;
|
|
12
12
|
}
|
|
13
|
+
/** T1.4.1 — runtime secret health probe result for a single credential row. */
|
|
14
|
+
export interface CredentialHealthProbe {
|
|
15
|
+
platformId: string;
|
|
16
|
+
state: CredentialState | "decrypt_failed";
|
|
17
|
+
keyHealth: "missing_key" | "wrong_key" | "ok";
|
|
18
|
+
hasBaseUrl: boolean;
|
|
19
|
+
diagnosticCode: "missing_runtime_secret" | "credential_recovery_required" | "ok";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* T1.4.1 — probe a credential record for runtime secret health.
|
|
23
|
+
*
|
|
24
|
+
* Given a raw encrypted value from the DB, this function checks:
|
|
25
|
+
* 1. Is SECOND_NATURE_ENCRYPTION_KEY present and >= 32 chars?
|
|
26
|
+
* 2. Can the ciphertext be decrypted with that key?
|
|
27
|
+
*
|
|
28
|
+
* It never throws; all failures are encoded in the returned state.
|
|
29
|
+
*/
|
|
30
|
+
export declare function probeCredentialHealth(platformId: string, encryptedValue: string | undefined | null, baseUrl: string | undefined | null): CredentialHealthProbe;
|
|
13
31
|
export declare function createCredentialVault(db: StateDatabase["db"]): CredentialVault;
|
|
@@ -54,6 +54,58 @@ export function decryptCredentialAtRest(ciphertext) {
|
|
|
54
54
|
return "";
|
|
55
55
|
return decryptInternal(ciphertext);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* T1.4.1 — probe a credential record for runtime secret health.
|
|
59
|
+
*
|
|
60
|
+
* Given a raw encrypted value from the DB, this function checks:
|
|
61
|
+
* 1. Is SECOND_NATURE_ENCRYPTION_KEY present and >= 32 chars?
|
|
62
|
+
* 2. Can the ciphertext be decrypted with that key?
|
|
63
|
+
*
|
|
64
|
+
* It never throws; all failures are encoded in the returned state.
|
|
65
|
+
*/
|
|
66
|
+
export function probeCredentialHealth(platformId, encryptedValue, baseUrl) {
|
|
67
|
+
// Key availability check
|
|
68
|
+
const rawKey = process.env.SECOND_NATURE_ENCRYPTION_KEY?.trim();
|
|
69
|
+
if (!rawKey || rawKey.length < 32) {
|
|
70
|
+
return {
|
|
71
|
+
platformId,
|
|
72
|
+
state: encryptedValue ? "decrypt_failed" : "missing",
|
|
73
|
+
keyHealth: "missing_key",
|
|
74
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
75
|
+
diagnosticCode: "missing_runtime_secret",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// No encrypted value to test
|
|
79
|
+
if (!encryptedValue) {
|
|
80
|
+
return {
|
|
81
|
+
platformId,
|
|
82
|
+
state: "missing",
|
|
83
|
+
keyHealth: "ok",
|
|
84
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
85
|
+
diagnosticCode: "ok",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Decryption attempt
|
|
89
|
+
try {
|
|
90
|
+
decryptCredentialAtRest(encryptedValue);
|
|
91
|
+
return {
|
|
92
|
+
platformId,
|
|
93
|
+
state: "active",
|
|
94
|
+
keyHealth: "ok",
|
|
95
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
96
|
+
diagnosticCode: "ok",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {
|
|
101
|
+
platformId,
|
|
102
|
+
state: "decrypt_failed",
|
|
103
|
+
keyHealth: "wrong_key",
|
|
104
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
105
|
+
diagnosticCode: "credential_recovery_required",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
57
109
|
export function createCredentialVault(db) {
|
|
58
110
|
return {
|
|
59
111
|
async saveCredentialContext(input) {
|
|
@@ -89,16 +141,34 @@ export function createCredentialVault(db) {
|
|
|
89
141
|
if (!record)
|
|
90
142
|
return null;
|
|
91
143
|
let plain;
|
|
144
|
+
let status = record.status;
|
|
92
145
|
if (record.encryptedValue) {
|
|
93
146
|
if (!isCredentialCiphertext(record.encryptedValue)) {
|
|
94
|
-
|
|
147
|
+
// Fail-closed: return decrypt_failed so callers do not crash.
|
|
148
|
+
return {
|
|
149
|
+
platformId: record.platformId,
|
|
150
|
+
credentialType: record.credentialType,
|
|
151
|
+
status: "decrypt_failed",
|
|
152
|
+
encryptedValue: undefined,
|
|
153
|
+
verificationCode: record.verificationCode ?? undefined,
|
|
154
|
+
challengeText: record.challengeText ?? undefined,
|
|
155
|
+
verificationDeadline: record.expiresAt ?? undefined,
|
|
156
|
+
attemptsRemaining: record.attemptsRemaining ?? undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
plain = decryptCredentialAtRest(record.encryptedValue);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Decryption failure must not break the whole state load.
|
|
164
|
+
status = "decrypt_failed";
|
|
165
|
+
plain = undefined;
|
|
95
166
|
}
|
|
96
|
-
plain = decryptCredentialAtRest(record.encryptedValue);
|
|
97
167
|
}
|
|
98
168
|
return {
|
|
99
169
|
platformId: record.platformId,
|
|
100
170
|
credentialType: record.credentialType,
|
|
101
|
-
status
|
|
171
|
+
status,
|
|
102
172
|
encryptedValue: plain,
|
|
103
173
|
verificationCode: record.verificationCode ?? undefined,
|
|
104
174
|
challengeText: record.challengeText ?? undefined,
|