@haaaiawd/second-nature 0.2.2 → 0.2.5
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
- package/runtime/cli/ops/heartbeat-surface.js +72 -1
- package/runtime/cli/ops/ops-router.js +119 -31
- package/runtime/connectors/base/contract.d.ts +11 -0
- package/runtime/connectors/base/failure-taxonomy.js +45 -26
- package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
- package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
- package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
- package/runtime/connectors/services/connector-cooldown-port.js +123 -0
- package/runtime/connectors/services/connector-executor-adapter.js +10 -4
- package/runtime/connectors/services/credential-route-context.d.ts +3 -2
- package/runtime/connectors/services/credential-route-context.js +19 -3
- package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
- package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
- package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
- package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
- package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
- package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
- package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
- package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
- package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
- package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
- package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
- package/runtime/core/second-nature/perception/perception-builder.js +18 -7
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
- package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
- package/runtime/observability/living-loop-health-gate.d.ts +49 -0
- package/runtime/observability/living-loop-health-gate.js +141 -0
- package/runtime/observability/loop-status.d.ts +30 -0
- package/runtime/observability/loop-status.js +167 -7
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
- package/runtime/shared/types/v8-contracts.d.ts +2 -2
- package/runtime/storage/db/index.js +60 -6
- package/runtime/storage/db/migrations/index.js +4 -0
- package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
- package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
- package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
- package/runtime/storage/db/schema/v8-entities.js +62 -1
- package/runtime/storage/v8-state-stores.d.ts +41 -2
- package/runtime/storage/v8-state-stores.js +206 -2
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ import type { GoalLifecyclePolicy } from "../../core/second-nature/heartbeat/goa
|
|
|
19
19
|
import type { IdleCuriosityPolicy } from "../../core/second-nature/heartbeat/idle-curiosity-policy.js";
|
|
20
20
|
import type { CircuitBreakerManager } from "../../core/second-nature/body/circuit-breaker/circuit-breaker-manager.js";
|
|
21
21
|
import type { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
22
|
+
import { type RealRuntimeSpineResult } from "../../core/second-nature/control-plane/real-runtime-spine.js";
|
|
22
23
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
23
24
|
export interface HeartbeatSurfaceResult {
|
|
24
25
|
ok: boolean;
|
|
@@ -33,6 +34,20 @@ export interface HeartbeatSurfaceResult {
|
|
|
33
34
|
livedExperienceLoopClaimed: boolean;
|
|
34
35
|
/** True when structured fields mirror a fake adapter for schema parity only */
|
|
35
36
|
schemaParityOnly?: boolean;
|
|
37
|
+
/** T-CP.R.2: v8 real runtime spine result when state-backed action-closure spine ran */
|
|
38
|
+
v8Spine?: RealRuntimeSpineResult & {
|
|
39
|
+
degradedReason?: string;
|
|
40
|
+
};
|
|
41
|
+
/** T-GVS.R.1: agent-facing impulse context artifact read pointer */
|
|
42
|
+
impulseContext?: {
|
|
43
|
+
available: boolean;
|
|
44
|
+
sceneType?: string;
|
|
45
|
+
capabilityClass?: string | null;
|
|
46
|
+
impulseText?: string | null;
|
|
47
|
+
atmosphereText?: string | null;
|
|
48
|
+
freshnessMs?: number;
|
|
49
|
+
missingReason?: string;
|
|
50
|
+
};
|
|
36
51
|
}
|
|
37
52
|
export interface HeartbeatCheckInput {
|
|
38
53
|
probeOnly?: boolean;
|
|
@@ -83,5 +98,10 @@ export interface HeartbeatCheckInput {
|
|
|
83
98
|
circuitBreakerManager?: CircuitBreakerManager;
|
|
84
99
|
/** T-OBS.R.1: shared audit sink for connector/Quiet events consumed by heartbeat_digest. */
|
|
85
100
|
auditStore?: AppendOnlyAuditStore;
|
|
101
|
+
/**
|
|
102
|
+
* T-CP.R.2: when true and state DB is wired, runs the v8 real runtime action-closure spine
|
|
103
|
+
* in addition to the v7 heartbeat loop. Produces state-backed closure/no-action records.
|
|
104
|
+
*/
|
|
105
|
+
v8SpineEnabled?: boolean;
|
|
86
106
|
}
|
|
87
107
|
export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createWorkspaceHeartbeatRunner } from "./workspace-heartbeat-runner.js";
|
|
2
|
+
// T-CP.R.2: v8 real runtime spine bridge
|
|
3
|
+
import { runRealRuntimeHeartbeatCycle, } from "../../core/second-nature/control-plane/real-runtime-spine.js";
|
|
2
4
|
function mapCycleToSurface(cycle, surfaceMode) {
|
|
3
5
|
const status = cycle.status === "runtime_carrier_only"
|
|
4
6
|
? "runtime_carrier_only"
|
|
@@ -86,7 +88,76 @@ export async function heartbeatCheck(input) {
|
|
|
86
88
|
});
|
|
87
89
|
try {
|
|
88
90
|
const cycle = await run(signal);
|
|
89
|
-
|
|
91
|
+
const surfaceResult = mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
92
|
+
// T-CP.R.2: run v8 real runtime spine when enabled and state is available
|
|
93
|
+
if (input.v8SpineEnabled && input.state && input.workspaceRoot) {
|
|
94
|
+
try {
|
|
95
|
+
const v8Result = await runRealRuntimeHeartbeatCycle({
|
|
96
|
+
workspaceRoot: input.workspaceRoot,
|
|
97
|
+
state: input.state,
|
|
98
|
+
requestedAt: timestamp,
|
|
99
|
+
trigger: "host",
|
|
100
|
+
});
|
|
101
|
+
if ("status" in v8Result && v8Result.status === "degraded") {
|
|
102
|
+
surfaceResult.v8Spine = {
|
|
103
|
+
cycleId: "",
|
|
104
|
+
cycleSequence: 0,
|
|
105
|
+
degradedReason: v8Result.reason,
|
|
106
|
+
};
|
|
107
|
+
surfaceResult.reasons = [
|
|
108
|
+
...surfaceResult.reasons,
|
|
109
|
+
`v8_spine_degraded:${v8Result.reason}`,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const spine = v8Result;
|
|
114
|
+
surfaceResult.v8Spine = spine;
|
|
115
|
+
surfaceResult.reasons = [
|
|
116
|
+
...surfaceResult.reasons,
|
|
117
|
+
`v8_spine_cycle:${spine.cycleId}`,
|
|
118
|
+
spine.closureRef
|
|
119
|
+
? "v8_closure_recorded"
|
|
120
|
+
: `v8_no_action:${spine.noActionReason ?? "unknown"}`,
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (v8Err) {
|
|
125
|
+
const v8Msg = v8Err instanceof Error ? v8Err.message : String(v8Err);
|
|
126
|
+
surfaceResult.reasons = [
|
|
127
|
+
...surfaceResult.reasons,
|
|
128
|
+
`v8_spine_exception:${v8Msg.slice(0, 120)}`,
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// T-GVS.R.1: expose impulse context artifact when state is available
|
|
133
|
+
if (input.state) {
|
|
134
|
+
try {
|
|
135
|
+
const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
|
|
136
|
+
const ctx = await readImpulseContext(input.state, "social");
|
|
137
|
+
if (ctx.available) {
|
|
138
|
+
surfaceResult.impulseContext = {
|
|
139
|
+
available: true,
|
|
140
|
+
sceneType: ctx.artifact.sceneType,
|
|
141
|
+
capabilityClass: ctx.artifact.capabilityClass,
|
|
142
|
+
impulseText: ctx.artifact.impulseText,
|
|
143
|
+
atmosphereText: ctx.artifact.atmosphereText,
|
|
144
|
+
freshnessMs: ctx.freshnessMs,
|
|
145
|
+
};
|
|
146
|
+
surfaceResult.reasons.push(`impulse_context:${ctx.artifact.id}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
surfaceResult.impulseContext = {
|
|
150
|
+
available: false,
|
|
151
|
+
missingReason: ctx.reason,
|
|
152
|
+
};
|
|
153
|
+
surfaceResult.reasons.push(`impulse_context_missing:${ctx.reason}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Non-fatal: impulse context is advisory
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return surfaceResult;
|
|
90
161
|
}
|
|
91
162
|
catch (err) {
|
|
92
163
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -28,6 +28,7 @@ import { writeRestoreAudit, } from "../../observability/services/restore-audit-s
|
|
|
28
28
|
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
29
29
|
// v8 T-ROS.C.1: loop_status read model
|
|
30
30
|
import { readLoopStatus } from "../../observability/loop-status.js";
|
|
31
|
+
import { checkRealRunHealth } from "../../observability/living-loop-health-gate.js";
|
|
31
32
|
// T-ROS.C.3: ManualRunDispatcher and its deps
|
|
32
33
|
import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
|
|
33
34
|
import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
@@ -445,6 +446,8 @@ export function createOpsRouter(deps) {
|
|
|
445
446
|
goalLifecyclePolicy,
|
|
446
447
|
idleCuriosityPolicy,
|
|
447
448
|
circuitBreakerManager,
|
|
449
|
+
v8SpineEnabled: input
|
|
450
|
+
?.v8SpineEnabled ?? (deps.state !== undefined),
|
|
448
451
|
});
|
|
449
452
|
if (result.ok &&
|
|
450
453
|
result.surfaceMode === "workspace_full_runtime" &&
|
|
@@ -1053,6 +1056,37 @@ export function createOpsRouter(deps) {
|
|
|
1053
1056
|
...deps.heartbeatDigestDeps,
|
|
1054
1057
|
};
|
|
1055
1058
|
const digest = await generateHeartbeatDigest(date, digestDeps);
|
|
1059
|
+
// T-OBS.R.3: Embed real-run health into digest when state DB is available
|
|
1060
|
+
if (deps.state) {
|
|
1061
|
+
const realRunResult = await checkRealRunHealth(deps.state, date);
|
|
1062
|
+
if (realRunResult.ok) {
|
|
1063
|
+
digest.realRunHealth = {
|
|
1064
|
+
gatePassed: realRunResult.gate.gatePassed,
|
|
1065
|
+
contractSmokeOnly: realRunResult.gate.contractSmokeOnly,
|
|
1066
|
+
seededStateDetected: realRunResult.gate.seededStateDetected,
|
|
1067
|
+
hasRealClosure: realRunResult.gate.hasRealClosure,
|
|
1068
|
+
hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
|
|
1069
|
+
hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
|
|
1070
|
+
hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
|
|
1071
|
+
hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
|
|
1072
|
+
missingStage: realRunResult.gate.missingStage,
|
|
1073
|
+
missingReason: realRunResult.gate.missingReason,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
digest.realRunHealth = {
|
|
1078
|
+
gatePassed: false,
|
|
1079
|
+
contractSmokeOnly: false,
|
|
1080
|
+
seededStateDetected: false,
|
|
1081
|
+
hasRealClosure: false,
|
|
1082
|
+
hasQuietArtifact: false,
|
|
1083
|
+
hasDreamArtifact: false,
|
|
1084
|
+
hasFreshImpulseContext: false,
|
|
1085
|
+
hasProjectionFeedback: false,
|
|
1086
|
+
missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1056
1090
|
const envelope = {
|
|
1057
1091
|
ok: true,
|
|
1058
1092
|
command: "heartbeat_digest",
|
|
@@ -1472,14 +1506,12 @@ export function createOpsRouter(deps) {
|
|
|
1472
1506
|
return envelope;
|
|
1473
1507
|
}
|
|
1474
1508
|
}
|
|
1475
|
-
// ─── T-V7C.C.4R: guidance_payload
|
|
1509
|
+
// ─── T-V7C.C.4R + T-GVS.R.1: guidance_payload ─────────────────────────
|
|
1476
1510
|
// Returns the assembled impulse + atmosphere for a given scene context.
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1511
|
+
// When state DB is wired, reads persisted artifact first; falls back to
|
|
1512
|
+
// real-time assembly and persists for subsequent reads.
|
|
1479
1513
|
if (command === "guidance_payload") {
|
|
1480
1514
|
const generatedAt = new Date().toISOString();
|
|
1481
|
-
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1482
|
-
const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1483
1515
|
const sceneType = input?.sceneType ?? "social";
|
|
1484
1516
|
const capabilityIntent = typeof input?.capabilityIntent === "string"
|
|
1485
1517
|
? input.capabilityIntent
|
|
@@ -1505,22 +1537,53 @@ export function createOpsRouter(deps) {
|
|
|
1505
1537
|
};
|
|
1506
1538
|
return envelope;
|
|
1507
1539
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1540
|
+
// T-GVS.R.1: Try reading persisted artifact first
|
|
1541
|
+
let artifactData;
|
|
1542
|
+
let warnings = [];
|
|
1543
|
+
let sourceRefs = [
|
|
1544
|
+
"guidance/capability-class.ts",
|
|
1545
|
+
"guidance/impulse-assembler.ts",
|
|
1546
|
+
"guidance/template-registry.ts",
|
|
1547
|
+
"guidance/output-guard.ts",
|
|
1548
|
+
];
|
|
1549
|
+
if (deps.state) {
|
|
1550
|
+
try {
|
|
1551
|
+
const { readImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-reader.js");
|
|
1552
|
+
const existing = await readImpulseContext(deps.state, sceneType, capabilityIntent, platformId);
|
|
1553
|
+
if (existing.available) {
|
|
1554
|
+
artifactData = {
|
|
1555
|
+
sceneType: existing.artifact.sceneType,
|
|
1556
|
+
capabilityIntent: existing.artifact.capabilityIntent,
|
|
1557
|
+
platformId: existing.artifact.platformId,
|
|
1558
|
+
capabilityClass: existing.artifact.capabilityClass,
|
|
1559
|
+
impulseSource: existing.artifact.impulseSource,
|
|
1560
|
+
impulseText: existing.artifact.impulseText,
|
|
1561
|
+
atmosphereText: existing.artifact.atmosphereText,
|
|
1562
|
+
expressionBoundaryConstraints: existing.artifact.expressionBoundaryConstraints,
|
|
1563
|
+
expressionBoundaryStyle: existing.artifact.expressionBoundaryStyle,
|
|
1564
|
+
freshnessMs: existing.freshnessMs,
|
|
1565
|
+
persisted: true,
|
|
1566
|
+
};
|
|
1567
|
+
sourceRefs.push("core/second-nature/guidance/impulse-context-reader.ts");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
// Reader failure → fall through to assembly
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
// Real-time assembly if no persisted artifact
|
|
1575
|
+
if (!artifactData) {
|
|
1576
|
+
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1577
|
+
const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
|
|
1578
|
+
const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1579
|
+
const impulseResult = assembleImpulseSync({
|
|
1580
|
+
sceneType: sceneType,
|
|
1581
|
+
capabilityIntent,
|
|
1582
|
+
platformId,
|
|
1583
|
+
});
|
|
1584
|
+
const atmosphere = getShortAtmosphereTemplate("active", "low");
|
|
1585
|
+
const expressionBoundary = buildExpressionBoundary(sceneType);
|
|
1586
|
+
artifactData = {
|
|
1524
1587
|
sceneType,
|
|
1525
1588
|
capabilityIntent: capabilityIntent ?? null,
|
|
1526
1589
|
platformId: platformId ?? null,
|
|
@@ -1532,16 +1595,41 @@ export function createOpsRouter(deps) {
|
|
|
1532
1595
|
atmosphereReviewStatus: atmosphere.reviewStatus,
|
|
1533
1596
|
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1534
1597
|
expressionBoundaryStyle: expressionBoundary.style,
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1598
|
+
persisted: false,
|
|
1599
|
+
};
|
|
1600
|
+
if (impulseResult.source === "none") {
|
|
1601
|
+
warnings.push("no_impulse_available_for_this_scene_and_capability");
|
|
1602
|
+
}
|
|
1603
|
+
// T-GVS.R.1: Persist assembled artifact for future reads
|
|
1604
|
+
if (deps.state) {
|
|
1605
|
+
try {
|
|
1606
|
+
const { writeImpulseContext } = await import("../../core/second-nature/guidance/impulse-context-writer.js");
|
|
1607
|
+
await writeImpulseContext(deps.state, {
|
|
1608
|
+
sceneType,
|
|
1609
|
+
capabilityIntent,
|
|
1610
|
+
platformId,
|
|
1611
|
+
impulseResult,
|
|
1612
|
+
atmosphereText: atmosphere.text,
|
|
1613
|
+
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1614
|
+
expressionBoundaryStyle: expressionBoundary.style,
|
|
1615
|
+
}, { now: generatedAt });
|
|
1616
|
+
sourceRefs.push("core/second-nature/guidance/impulse-context-writer.ts");
|
|
1617
|
+
}
|
|
1618
|
+
catch {
|
|
1619
|
+
// Persistence failure is non-fatal; surface still returns assembled payload
|
|
1620
|
+
warnings.push("impulse_context_persistence_failed");
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const envelope = {
|
|
1625
|
+
ok: true,
|
|
1626
|
+
command: "guidance_payload",
|
|
1627
|
+
runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
|
|
1628
|
+
surfaceMode: "cli",
|
|
1629
|
+
generatedAt,
|
|
1630
|
+
data: artifactData,
|
|
1631
|
+
warnings,
|
|
1632
|
+
sourceRefs,
|
|
1545
1633
|
};
|
|
1546
1634
|
return envelope;
|
|
1547
1635
|
}
|
|
@@ -12,6 +12,14 @@ export interface ConnectorRequestIdentity {
|
|
|
12
12
|
/** Canonical name across all platforms. */
|
|
13
13
|
canonicalName?: string;
|
|
14
14
|
}
|
|
15
|
+
export interface PolicyProof {
|
|
16
|
+
decisionId: string;
|
|
17
|
+
decision: "allow" | "defer" | "downgrade" | "deny";
|
|
18
|
+
ownerConfirmMode?: boolean;
|
|
19
|
+
ownerConfirmed?: boolean;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
15
23
|
export interface ConnectorRequest {
|
|
16
24
|
platformId: string;
|
|
17
25
|
intent: CapabilityIntent;
|
|
@@ -23,6 +31,8 @@ export interface ConnectorRequest {
|
|
|
23
31
|
intentId?: string;
|
|
24
32
|
/** T-V7C.C.4: identity for connector request (readable, no credential). */
|
|
25
33
|
identity?: ConnectorRequestIdentity;
|
|
34
|
+
/** T-CS.R.1: policy proof for write-side actions */
|
|
35
|
+
policyProof?: PolicyProof;
|
|
26
36
|
}
|
|
27
37
|
export interface ExecutionPlan {
|
|
28
38
|
platformId: string;
|
|
@@ -63,6 +73,7 @@ export interface CooldownLedgerPort {
|
|
|
63
73
|
loadCooldownState(platformId: string, intent: CapabilityIntent): Promise<{
|
|
64
74
|
blocked: boolean;
|
|
65
75
|
retryAfterMs?: number;
|
|
76
|
+
reason?: string;
|
|
66
77
|
}>;
|
|
67
78
|
}
|
|
68
79
|
export interface RouteContextPort extends CredentialContextPort, CooldownLedgerPort {
|
|
@@ -61,6 +61,23 @@ function readRetryAfterMs(input) {
|
|
|
61
61
|
}
|
|
62
62
|
return undefined;
|
|
63
63
|
}
|
|
64
|
+
function readStatusCode(record) {
|
|
65
|
+
if (typeof record.status === "number")
|
|
66
|
+
return record.status;
|
|
67
|
+
if (typeof record.statusCode === "number")
|
|
68
|
+
return record.statusCode;
|
|
69
|
+
if (typeof record.status === "string") {
|
|
70
|
+
const parsed = Number.parseInt(record.status, 10);
|
|
71
|
+
if (Number.isFinite(parsed))
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.statusCode === "string") {
|
|
75
|
+
const parsed = Number.parseInt(record.statusCode, 10);
|
|
76
|
+
if (Number.isFinite(parsed))
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
64
81
|
export function classifyFailure(error) {
|
|
65
82
|
if (error instanceof ConnectorPolicyError) {
|
|
66
83
|
return {
|
|
@@ -74,6 +91,34 @@ export function classifyFailure(error) {
|
|
|
74
91
|
}
|
|
75
92
|
if (error && typeof error === "object") {
|
|
76
93
|
const record = error;
|
|
94
|
+
const status = readStatusCode(record);
|
|
95
|
+
if (status !== undefined) {
|
|
96
|
+
if (status === 429) {
|
|
97
|
+
return {
|
|
98
|
+
class: "rate_limited",
|
|
99
|
+
retryable: RETRYABLE_BY_CLASS.rate_limited,
|
|
100
|
+
retryAfterMs: readRetryAfterMs(record),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (status === 401 || status === 403) {
|
|
104
|
+
return {
|
|
105
|
+
class: "auth_failure",
|
|
106
|
+
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (status === 400 || status === 404 || status === 422) {
|
|
110
|
+
return {
|
|
111
|
+
class: "permanent_input_error",
|
|
112
|
+
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (status >= 500 && status <= 599) {
|
|
116
|
+
return {
|
|
117
|
+
class: "transport_failure",
|
|
118
|
+
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
77
122
|
const code = record.code;
|
|
78
123
|
if (typeof code === "string") {
|
|
79
124
|
if (code === "auth_failure")
|
|
@@ -152,32 +197,6 @@ export function classifyFailure(error) {
|
|
|
152
197
|
retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
|
|
153
198
|
};
|
|
154
199
|
}
|
|
155
|
-
const status = record.status;
|
|
156
|
-
if (status === 429) {
|
|
157
|
-
return {
|
|
158
|
-
class: "rate_limited",
|
|
159
|
-
retryable: RETRYABLE_BY_CLASS.rate_limited,
|
|
160
|
-
retryAfterMs: readRetryAfterMs(record),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (status === 401 || status === 403) {
|
|
164
|
-
return {
|
|
165
|
-
class: "auth_failure",
|
|
166
|
-
retryable: RETRYABLE_BY_CLASS.auth_failure,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
if (status === 400 || status === 404 || status === 422) {
|
|
170
|
-
return {
|
|
171
|
-
class: "permanent_input_error",
|
|
172
|
-
retryable: RETRYABLE_BY_CLASS.permanent_input_error,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (status === 500 || status === 502 || status === 503 || status === 504) {
|
|
176
|
-
return {
|
|
177
|
-
class: "transport_failure",
|
|
178
|
-
retryable: RETRYABLE_BY_CLASS.transport_failure,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
200
|
}
|
|
182
201
|
return {
|
|
183
202
|
class: "unknown_platform_change",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Verify policy proof before external write; reject without platform
|
|
5
|
+
* call when proof is missing, deny, or lacks owner confirmation for high-risk
|
|
6
|
+
* actions. Support dry-run mode for safe testing.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
|
|
11
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
|
|
15
|
+
* - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT bypass ActionPolicyDecision.
|
|
19
|
+
* - Does NOT execute write without valid policy proof.
|
|
20
|
+
* - Does NOT leak credentials in returned results.
|
|
21
|
+
*/
|
|
22
|
+
import type { ConnectorRequest, ConnectorResult } from "./contract.js";
|
|
23
|
+
export interface WriteDispatchResult {
|
|
24
|
+
status: "allowed" | "denied" | "deferred" | "dry_run" | "downgraded";
|
|
25
|
+
reason: string;
|
|
26
|
+
connectorResult?: ConnectorResult<unknown>;
|
|
27
|
+
simulatedPayload?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export declare function dispatchPolicyBoundWrite(request: ConnectorRequest, executeConnector: (req: ConnectorRequest) => Promise<ConnectorResult<unknown>>): Promise<WriteDispatchResult>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PolicyBoundWriteDispatch — Write-side connector dispatch with policy proof gate.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Verify policy proof before external write; reject without platform
|
|
5
|
+
* call when proof is missing, deny, or lacks owner confirmation for high-risk
|
|
6
|
+
* actions. Support dry-run mode for safe testing.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §2`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.3`
|
|
11
|
+
* - `docs/validation/openclaw-plugin-classification.md §5`
|
|
12
|
+
*
|
|
13
|
+
* Dependencies:
|
|
14
|
+
* - `src/connectors/base/contract.js` (ConnectorRequest, ConnectorResult, PolicyProof)
|
|
15
|
+
* - `src/connectors/base/policy-layer.js` (createConnectorPolicyLayer)
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT bypass ActionPolicyDecision.
|
|
19
|
+
* - Does NOT execute write without valid policy proof.
|
|
20
|
+
* - Does NOT leak credentials in returned results.
|
|
21
|
+
*/
|
|
22
|
+
// ───────────────────────────────────────────────────────────────
|
|
23
|
+
// Helpers
|
|
24
|
+
// ───────────────────────────────────────────────────────────────
|
|
25
|
+
function isWriteCapability(intent) {
|
|
26
|
+
const writeIntents = ["post.publish", "comment.reply", "message.send"];
|
|
27
|
+
return writeIntents.includes(intent);
|
|
28
|
+
}
|
|
29
|
+
function validatePolicyProof(proof, intent) {
|
|
30
|
+
if (!proof) {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
reason: "policy_denied_missing_permission",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (proof.decision === "deny") {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
reason: proof.reason || "policy_denied_high_risk",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (proof.decision === "defer") {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
reason: "policy_deferred_owner_confirmation",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (proof.decision === "downgrade") {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
reason: proof.reason || "policy_downgraded_to_draft",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Allow decision
|
|
55
|
+
if (proof.decision !== "allow") {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
reason: "policy_denied_missing_permission",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// For write capabilities, require owner-confirm, dry-run, or explicit owner-confirmed flag
|
|
62
|
+
if (isWriteCapability(intent) &&
|
|
63
|
+
!proof.ownerConfirmMode &&
|
|
64
|
+
!proof.dryRun &&
|
|
65
|
+
!proof.ownerConfirmed) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
reason: "policy_denied_owner_confirm_required",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { valid: true, reason: "policy_allowed" };
|
|
72
|
+
}
|
|
73
|
+
// ───────────────────────────────────────────────────────────────
|
|
74
|
+
// Public API
|
|
75
|
+
// ───────────────────────────────────────────────────────────────
|
|
76
|
+
export async function dispatchPolicyBoundWrite(request, executeConnector) {
|
|
77
|
+
const { intent, policyProof, payload } = request;
|
|
78
|
+
// Only gate write capabilities
|
|
79
|
+
if (!isWriteCapability(intent)) {
|
|
80
|
+
// Read capabilities pass through to normal execution
|
|
81
|
+
const result = await executeConnector(request);
|
|
82
|
+
return {
|
|
83
|
+
status: "allowed",
|
|
84
|
+
reason: "read_capability_no_policy_gate",
|
|
85
|
+
connectorResult: result,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Validate policy proof
|
|
89
|
+
const validation = validatePolicyProof(policyProof, intent);
|
|
90
|
+
if (!validation.valid) {
|
|
91
|
+
return {
|
|
92
|
+
status: validation.reason === "policy_deferred_owner_confirmation"
|
|
93
|
+
? "deferred"
|
|
94
|
+
: "denied",
|
|
95
|
+
reason: validation.reason,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Dry-run mode: simulate execution without platform call
|
|
99
|
+
if (policyProof?.dryRun) {
|
|
100
|
+
return {
|
|
101
|
+
status: "dry_run",
|
|
102
|
+
reason: "dry_run_simulated_success",
|
|
103
|
+
simulatedPayload: {
|
|
104
|
+
...payload,
|
|
105
|
+
_simulated: true,
|
|
106
|
+
_idempotencyKey: request.idempotencyKey,
|
|
107
|
+
_decisionId: policyProof.decisionId,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Owner-confirm mode without explicit approval: defer to owner approval
|
|
112
|
+
if (policyProof?.ownerConfirmMode && !policyProof?.ownerConfirmed) {
|
|
113
|
+
return {
|
|
114
|
+
status: "deferred",
|
|
115
|
+
reason: "owner_confirm_pending",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Full allow → execute connector
|
|
119
|
+
const result = await executeConnector(request);
|
|
120
|
+
return {
|
|
121
|
+
status: result.status === "success" ? "allowed" : "denied",
|
|
122
|
+
reason: result.status === "success"
|
|
123
|
+
? "execution_completed"
|
|
124
|
+
: (result.failureClass || "execution_failed"),
|
|
125
|
+
connectorResult: result,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Track terminal failures per platform/capability and block replay
|
|
5
|
+
* for a bounded window after repeated failures. Successful recovery is allowed
|
|
6
|
+
* to bypass stale cooldown.
|
|
7
|
+
*
|
|
8
|
+
* Design authority:
|
|
9
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
|
|
10
|
+
* - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
|
|
11
|
+
*
|
|
12
|
+
* Dependencies:
|
|
13
|
+
* - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
|
|
14
|
+
* - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
|
|
15
|
+
*
|
|
16
|
+
* Boundary:
|
|
17
|
+
* - Does not execute connectors; only records/read cooldown state.
|
|
18
|
+
* - Does not permanently blacklist platforms; cooldown expires.
|
|
19
|
+
*/
|
|
20
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
21
|
+
import type { CooldownPort } from "../base/policy-layer.js";
|
|
22
|
+
export declare function createConnectorCooldownPort(db: StateDatabase): CooldownPort;
|