planning-with-files 3.11.2 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -11
- package/SKILL.md +25 -14
- package/extensions/planning-with-files/package.json +1 -1
- package/extensions/planning-with-files/runtime.ts +7 -7
- package/package.json +5 -2
- package/scripts/attest-plan.ps1 +381 -34
- package/scripts/init-session.sh +19 -0
- package/scripts/inject-plan.sh +692 -104
- package/scripts/phase-status.ps1 +79 -17
- package/scripts/phase-status.sh +60 -16
- package/scripts/resolve-plan-dir.ps1 +77 -7
- package/scripts/resolve-plan-dir.sh +6 -1
- package/scripts/session-catchup.py +131 -24
- package/scripts/verify-shell-line-endings.mjs +50 -0
- package/templates/analytics_findings.md +67 -85
- package/templates/analytics_task_plan.md +81 -106
- package/templates/findings.md +47 -95
- package/templates/progress.md +58 -114
- package/templates/task_plan.md +89 -140
package/scripts/attest-plan.ps1
CHANGED
|
@@ -31,42 +31,378 @@ param(
|
|
|
31
31
|
|
|
32
32
|
$ErrorActionPreference = "Stop"
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
$script:IsWindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT
|
|
35
|
+
if (-not $script:IsWindowsHost) {
|
|
36
|
+
throw "Safe no-follow descriptor operations are unavailable in this PowerShell script on Unix. Use scripts/attest-plan.sh instead."
|
|
37
|
+
}
|
|
38
|
+
if ($script:IsWindowsHost -and -not ("PwfAttestationNative" -as [type])) {
|
|
39
|
+
Add-Type -TypeDefinition @'
|
|
40
|
+
using System;
|
|
41
|
+
using System.ComponentModel;
|
|
42
|
+
using System.IO;
|
|
43
|
+
using System.Runtime.InteropServices;
|
|
44
|
+
using System.Text;
|
|
45
|
+
using Microsoft.Win32.SafeHandles;
|
|
46
|
+
|
|
47
|
+
public static class PwfAttestationNative {
|
|
48
|
+
private const uint GENERIC_READ = 0x80000000;
|
|
49
|
+
private const uint GENERIC_WRITE = 0x40000000;
|
|
50
|
+
private const uint DELETE = 0x00010000;
|
|
51
|
+
private const uint FILE_READ_ATTRIBUTES = 0x00000080;
|
|
52
|
+
private const uint FILE_SHARE_READ = 0x00000001;
|
|
53
|
+
private const uint FILE_SHARE_WRITE = 0x00000002;
|
|
54
|
+
private const uint FILE_SHARE_DELETE = 0x00000004;
|
|
55
|
+
private const uint CREATE_NEW = 1;
|
|
56
|
+
private const uint OPEN_EXISTING = 3;
|
|
57
|
+
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
|
58
|
+
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
|
59
|
+
private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
|
|
60
|
+
private const int FileAttributeTagInfo = 9;
|
|
61
|
+
private const int FileDispositionInfo = 4;
|
|
62
|
+
private const int ERROR_FILE_EXISTS = 80;
|
|
63
|
+
private const int ERROR_ALREADY_EXISTS = 183;
|
|
64
|
+
|
|
65
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
66
|
+
private struct FILE_ATTRIBUTE_TAG_INFO {
|
|
67
|
+
public uint FileAttributes;
|
|
68
|
+
public uint ReparseTag;
|
|
69
|
+
}
|
|
36
70
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
72
|
+
private struct BY_HANDLE_FILE_INFORMATION {
|
|
73
|
+
public uint FileAttributes;
|
|
74
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
|
|
75
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
|
|
76
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
|
|
77
|
+
public uint VolumeSerialNumber;
|
|
78
|
+
public uint FileSizeHigh;
|
|
79
|
+
public uint FileSizeLow;
|
|
80
|
+
public uint NumberOfLinks;
|
|
81
|
+
public uint FileIndexHigh;
|
|
82
|
+
public uint FileIndexLow;
|
|
41
83
|
}
|
|
42
84
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
86
|
+
private struct FILE_DISPOSITION_INFO {
|
|
87
|
+
[MarshalAs(UnmanagedType.Bool)] public bool DeleteFile;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
91
|
+
private static extern SafeFileHandle CreateFileW(
|
|
92
|
+
string name, uint access, uint share, IntPtr security,
|
|
93
|
+
uint creation, uint flags, IntPtr template);
|
|
94
|
+
|
|
95
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
96
|
+
private static extern bool GetFileInformationByHandleEx(
|
|
97
|
+
SafeFileHandle handle, int infoClass,
|
|
98
|
+
out FILE_ATTRIBUTE_TAG_INFO info, uint size);
|
|
99
|
+
|
|
100
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
101
|
+
private static extern bool GetFileInformationByHandle(
|
|
102
|
+
SafeFileHandle handle, out BY_HANDLE_FILE_INFORMATION info);
|
|
103
|
+
|
|
104
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
105
|
+
private static extern bool SetFileInformationByHandle(
|
|
106
|
+
SafeFileHandle handle, int infoClass,
|
|
107
|
+
ref FILE_DISPOSITION_INFO info, uint size);
|
|
108
|
+
|
|
109
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
110
|
+
private static extern uint GetFinalPathNameByHandleW(
|
|
111
|
+
SafeFileHandle handle, StringBuilder path, uint length, uint flags);
|
|
112
|
+
|
|
113
|
+
private static void ValidateRegular(SafeFileHandle handle, bool singleLink) {
|
|
114
|
+
FILE_ATTRIBUTE_TAG_INFO tag;
|
|
115
|
+
if (!GetFileInformationByHandleEx(
|
|
116
|
+
handle, FileAttributeTagInfo, out tag,
|
|
117
|
+
(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
|
|
118
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
119
|
+
if ((tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
120
|
+
throw new IOException("Refusing a reparse-point file.");
|
|
121
|
+
if ((tag.FileAttributes & (uint)FileAttributes.Directory) != 0)
|
|
122
|
+
throw new IOException("Refusing a directory where a regular file is required.");
|
|
123
|
+
if (singleLink) {
|
|
124
|
+
BY_HANDLE_FILE_INFORMATION info;
|
|
125
|
+
if (!GetFileInformationByHandle(handle, out info))
|
|
126
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
127
|
+
if (info.NumberOfLinks != 1)
|
|
128
|
+
throw new IOException("Refusing a multiply-linked attestation file.");
|
|
50
129
|
}
|
|
51
130
|
}
|
|
52
131
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
public static SafeFileHandle OpenRead(string path, bool singleLink) {
|
|
133
|
+
SafeFileHandle handle = CreateFileW(
|
|
134
|
+
path, GENERIC_READ | FILE_READ_ATTRIBUTES,
|
|
135
|
+
FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING,
|
|
136
|
+
FILE_FLAG_OPEN_REPARSE_POINT, IntPtr.Zero);
|
|
137
|
+
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
138
|
+
try { ValidateRegular(handle, singleLink); return handle; }
|
|
139
|
+
catch { handle.Dispose(); throw; }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public static SafeFileHandle OpenAttestationWrite(string path) {
|
|
143
|
+
uint access = GENERIC_READ | GENERIC_WRITE | DELETE | FILE_READ_ATTRIBUTES;
|
|
144
|
+
SafeFileHandle handle = CreateFileW(
|
|
145
|
+
path, access, 0, IntPtr.Zero, CREATE_NEW,
|
|
146
|
+
FILE_FLAG_OPEN_REPARSE_POINT, IntPtr.Zero);
|
|
147
|
+
if (handle.IsInvalid) {
|
|
148
|
+
int error = Marshal.GetLastWin32Error();
|
|
149
|
+
if (error != ERROR_FILE_EXISTS && error != ERROR_ALREADY_EXISTS)
|
|
150
|
+
throw new Win32Exception(error);
|
|
151
|
+
handle = CreateFileW(
|
|
152
|
+
path, access, 0, IntPtr.Zero, OPEN_EXISTING,
|
|
153
|
+
FILE_FLAG_OPEN_REPARSE_POINT, IntPtr.Zero);
|
|
154
|
+
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
61
155
|
}
|
|
156
|
+
try { ValidateRegular(handle, true); return handle; }
|
|
157
|
+
catch { handle.Dispose(); throw; }
|
|
62
158
|
}
|
|
63
159
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
160
|
+
public static SafeFileHandle OpenDelete(string path) {
|
|
161
|
+
SafeFileHandle handle = CreateFileW(
|
|
162
|
+
path, DELETE | FILE_READ_ATTRIBUTES, 0, IntPtr.Zero, OPEN_EXISTING,
|
|
163
|
+
FILE_FLAG_OPEN_REPARSE_POINT, IntPtr.Zero);
|
|
164
|
+
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
165
|
+
try { ValidateRegular(handle, true); return handle; }
|
|
166
|
+
catch { handle.Dispose(); throw; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public static void DeleteOpened(SafeFileHandle handle) {
|
|
170
|
+
FILE_DISPOSITION_INFO info = new FILE_DISPOSITION_INFO { DeleteFile = true };
|
|
171
|
+
if (!SetFileInformationByHandle(
|
|
172
|
+
handle, FileDispositionInfo, ref info,
|
|
173
|
+
(uint)Marshal.SizeOf(typeof(FILE_DISPOSITION_INFO))))
|
|
174
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public static string FinalPath(SafeFileHandle handle) {
|
|
178
|
+
StringBuilder buffer = new StringBuilder(32768);
|
|
179
|
+
uint length = GetFinalPathNameByHandleW(handle, buffer, (uint)buffer.Capacity, 0);
|
|
180
|
+
if (length == 0 || length >= buffer.Capacity)
|
|
181
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
182
|
+
string result = buffer.ToString();
|
|
183
|
+
if (result.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase))
|
|
184
|
+
return @"\\" + result.Substring(8);
|
|
185
|
+
if (result.StartsWith(@"\\?\", StringComparison.OrdinalIgnoreCase))
|
|
186
|
+
return result.Substring(4);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public static string FileIdentity(SafeFileHandle handle) {
|
|
191
|
+
BY_HANDLE_FILE_INFORMATION info;
|
|
192
|
+
if (!GetFileInformationByHandle(handle, out info))
|
|
193
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
194
|
+
return info.VolumeSerialNumber.ToString("X8") + ":" +
|
|
195
|
+
info.FileIndexHigh.ToString("X8") + info.FileIndexLow.ToString("X8");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public static string FinalDirectoryPath(string path) {
|
|
199
|
+
using (SafeFileHandle handle = OpenDirectory(path)) {
|
|
200
|
+
return FinalPath(handle);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public static SafeFileHandle OpenDirectory(string path) {
|
|
205
|
+
SafeFileHandle handle = CreateFileW(
|
|
206
|
+
path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
207
|
+
IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
|
|
208
|
+
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
209
|
+
return handle;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
'@
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
$script:SecurityRootHandle = $null
|
|
216
|
+
$script:SecurityRootFinalPath = $null
|
|
217
|
+
if ($script:IsWindowsHost) {
|
|
218
|
+
$securityRootPath = (Get-Location).Path
|
|
219
|
+
if ($env:PWF_PLAN_ROOT) {
|
|
220
|
+
$pin = $env:PWF_PLAN_ROOT
|
|
221
|
+
$isUnc = $pin.StartsWith('\\') -or $pin.StartsWith('//')
|
|
222
|
+
if (-not [IO.Path]::IsPathFullyQualified($pin) -or $isUnc) {
|
|
223
|
+
throw "[plan-attest] PWF_PLAN_ROOT must be an absolute local path."
|
|
224
|
+
}
|
|
225
|
+
$securityRootPath = $pin
|
|
226
|
+
}
|
|
227
|
+
$script:SecurityRootHandle = [PwfAttestationNative]::OpenDirectory($securityRootPath)
|
|
228
|
+
$script:SecurityRootFinalPath = [PwfAttestationNative]::FinalPath($script:SecurityRootHandle).TrimEnd('\', '/')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function Test-FinalPathWithinSecurityRoot {
|
|
232
|
+
param([string] $FinalPath)
|
|
233
|
+
$candidate = $FinalPath.TrimEnd('\', '/')
|
|
234
|
+
if ([string]::Equals($candidate, $script:SecurityRootFinalPath, [StringComparison]::OrdinalIgnoreCase)) {
|
|
235
|
+
return $true
|
|
236
|
+
}
|
|
237
|
+
return $candidate.StartsWith(
|
|
238
|
+
$script:SecurityRootFinalPath + [IO.Path]::DirectorySeparatorChar,
|
|
239
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Open-TrustedDirectory {
|
|
244
|
+
param([string] $ExpectedDirectory)
|
|
245
|
+
$handle = [PwfAttestationNative]::OpenDirectory($ExpectedDirectory)
|
|
246
|
+
try {
|
|
247
|
+
$finalPath = [PwfAttestationNative]::FinalPath($handle).TrimEnd('\', '/')
|
|
248
|
+
if (-not (Test-FinalPathWithinSecurityRoot $finalPath)) {
|
|
249
|
+
throw "Refusing a plan directory outside the frozen project root."
|
|
250
|
+
}
|
|
251
|
+
return [PSCustomObject]@{
|
|
252
|
+
Handle = $handle
|
|
253
|
+
FinalPath = $finalPath
|
|
254
|
+
Identity = [PwfAttestationNative]::FileIdentity($handle)
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
$handle.Dispose()
|
|
258
|
+
throw
|
|
67
259
|
}
|
|
260
|
+
}
|
|
68
261
|
|
|
69
|
-
|
|
262
|
+
function Assert-HandleParent {
|
|
263
|
+
param(
|
|
264
|
+
[Microsoft.Win32.SafeHandles.SafeFileHandle] $Handle,
|
|
265
|
+
[string] $ExpectedDirectoryFinal,
|
|
266
|
+
[string] $ExpectedDirectoryIdentity
|
|
267
|
+
)
|
|
268
|
+
if (-not $script:IsWindowsHost) { return }
|
|
269
|
+
$openedPath = [PwfAttestationNative]::FinalPath($Handle)
|
|
270
|
+
$openedParent = (Split-Path -Parent $openedPath).TrimEnd('\', '/')
|
|
271
|
+
if (-not [string]::Equals($openedParent, $ExpectedDirectoryFinal, [StringComparison]::OrdinalIgnoreCase)) {
|
|
272
|
+
throw "Refusing file outside the expected plan directory."
|
|
273
|
+
}
|
|
274
|
+
$parentHandle = [PwfAttestationNative]::OpenDirectory($openedParent)
|
|
275
|
+
try {
|
|
276
|
+
$openedIdentity = [PwfAttestationNative]::FileIdentity($parentHandle)
|
|
277
|
+
if (-not [string]::Equals($openedIdentity, $ExpectedDirectoryIdentity, [StringComparison]::Ordinal)) {
|
|
278
|
+
throw "Refusing a file whose parent directory identity changed."
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
$parentHandle.Dispose()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function Open-SafeReadStream {
|
|
286
|
+
param([string] $Path, [string] $ExpectedDirectory, [switch] $SingleLink)
|
|
287
|
+
if (-not $script:IsWindowsHost) {
|
|
288
|
+
throw "[plan-attest] Safe no-follow descriptor operations are unavailable in this PowerShell script on Unix. Use scripts/attest-plan.sh instead."
|
|
289
|
+
}
|
|
290
|
+
$directory = Open-TrustedDirectory -ExpectedDirectory $ExpectedDirectory
|
|
291
|
+
try {
|
|
292
|
+
$handle = [PwfAttestationNative]::OpenRead($Path, [bool]$SingleLink)
|
|
293
|
+
try {
|
|
294
|
+
Assert-HandleParent -Handle $handle -ExpectedDirectoryFinal $directory.FinalPath -ExpectedDirectoryIdentity $directory.Identity
|
|
295
|
+
return New-Object System.IO.FileStream($handle, [IO.FileAccess]::Read)
|
|
296
|
+
} catch {
|
|
297
|
+
$handle.Dispose()
|
|
298
|
+
throw
|
|
299
|
+
}
|
|
300
|
+
} finally {
|
|
301
|
+
$directory.Handle.Dispose()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function Read-SafeText {
|
|
306
|
+
param([string] $Path, [string] $ExpectedDirectory, [int64] $MaxBytes)
|
|
307
|
+
$stream = Open-SafeReadStream -Path $Path -ExpectedDirectory $ExpectedDirectory -SingleLink
|
|
308
|
+
try {
|
|
309
|
+
if ($stream.Length -gt $MaxBytes) { throw "Refusing oversized metadata file."
|
|
310
|
+
}
|
|
311
|
+
$buffer = New-Object byte[] ([int]$stream.Length)
|
|
312
|
+
$offset = 0
|
|
313
|
+
while ($offset -lt $buffer.Length) {
|
|
314
|
+
$read = $stream.Read($buffer, $offset, $buffer.Length - $offset)
|
|
315
|
+
if ($read -le 0) { break }
|
|
316
|
+
$offset += $read
|
|
317
|
+
}
|
|
318
|
+
return [Text.Encoding]::UTF8.GetString($buffer, 0, $offset)
|
|
319
|
+
} finally {
|
|
320
|
+
$stream.Dispose()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function Write-SafeAscii {
|
|
325
|
+
param([string] $Path, [string] $ExpectedDirectory, [string] $Value)
|
|
326
|
+
$bytes = [Text.Encoding]::ASCII.GetBytes($Value)
|
|
327
|
+
if (-not $script:IsWindowsHost) {
|
|
328
|
+
throw "[plan-attest] Safe no-follow descriptor operations are unavailable in this PowerShell script on Unix. Use scripts/attest-plan.sh instead."
|
|
329
|
+
}
|
|
330
|
+
$directory = Open-TrustedDirectory -ExpectedDirectory $ExpectedDirectory
|
|
331
|
+
try {
|
|
332
|
+
$handle = [PwfAttestationNative]::OpenAttestationWrite($Path)
|
|
333
|
+
try {
|
|
334
|
+
Assert-HandleParent -Handle $handle -ExpectedDirectoryFinal $directory.FinalPath -ExpectedDirectoryIdentity $directory.Identity
|
|
335
|
+
$stream = New-Object System.IO.FileStream($handle, [IO.FileAccess]::ReadWrite)
|
|
336
|
+
$handle = $null
|
|
337
|
+
try {
|
|
338
|
+
$stream.SetLength(0)
|
|
339
|
+
$stream.Write($bytes, 0, $bytes.Length)
|
|
340
|
+
$stream.Flush($true)
|
|
341
|
+
$stream.Position = 0
|
|
342
|
+
$verify = New-Object byte[] $bytes.Length
|
|
343
|
+
$read = $stream.Read($verify, 0, $verify.Length)
|
|
344
|
+
return [Text.Encoding]::ASCII.GetString($verify, 0, $read)
|
|
345
|
+
} finally {
|
|
346
|
+
$stream.Dispose()
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
if ($handle) { $handle.Dispose() }
|
|
350
|
+
}
|
|
351
|
+
} finally {
|
|
352
|
+
$directory.Handle.Dispose()
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function Remove-SafeFile {
|
|
357
|
+
param([string] $Path, [string] $ExpectedDirectory)
|
|
358
|
+
if (-not $script:IsWindowsHost) {
|
|
359
|
+
throw "[plan-attest] Safe no-follow descriptor operations are unavailable in this PowerShell script on Unix. Use scripts/attest-plan.sh instead."
|
|
360
|
+
}
|
|
361
|
+
$directory = Open-TrustedDirectory -ExpectedDirectory $ExpectedDirectory
|
|
362
|
+
try {
|
|
363
|
+
$handle = [PwfAttestationNative]::OpenDelete($Path)
|
|
364
|
+
try {
|
|
365
|
+
Assert-HandleParent -Handle $handle -ExpectedDirectoryFinal $directory.FinalPath -ExpectedDirectoryIdentity $directory.Identity
|
|
366
|
+
[PwfAttestationNative]::DeleteOpened($handle)
|
|
367
|
+
} finally {
|
|
368
|
+
$handle.Dispose()
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
$directory.Handle.Dispose()
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function Resolve-ContainedPlanFile {
|
|
376
|
+
param(
|
|
377
|
+
[string] $Candidate,
|
|
378
|
+
[string] $ExpectedDirectory
|
|
379
|
+
)
|
|
380
|
+
if (-not (Test-Path -LiteralPath $Candidate -PathType Leaf)) { return $null }
|
|
381
|
+
try {
|
|
382
|
+
$stream = Open-SafeReadStream -Path $Candidate -ExpectedDirectory $ExpectedDirectory
|
|
383
|
+
$stream.Dispose()
|
|
384
|
+
return (Get-Item -LiteralPath $Candidate -Force -ErrorAction Stop).FullName
|
|
385
|
+
} catch { return $null }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function Resolve-PlanFile {
|
|
389
|
+
$resolver = Join-Path $PSScriptRoot "resolve-plan-dir.ps1"
|
|
390
|
+
if (-not (Test-Path -LiteralPath $resolver -PathType Leaf)) { return $null }
|
|
391
|
+
$resolvedDir = @(& $resolver | Where-Object { $_ }) | Select-Object -First 1
|
|
392
|
+
if ($resolvedDir) {
|
|
393
|
+
$planFile = Join-Path $resolvedDir "task_plan.md"
|
|
394
|
+
return (Resolve-ContainedPlanFile -Candidate $planFile -ExpectedDirectory $resolvedDir)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# An explicit pin or a scoped selector that failed validation must never
|
|
398
|
+
# fall through and attest an unrelated legacy-root plan.
|
|
399
|
+
if ($env:PWF_PLAN_ROOT -or $env:PLAN_ID) { return $null }
|
|
400
|
+
$activePointer = Join-Path (Join-Path (Get-Location) ".planning") ".active_plan"
|
|
401
|
+
$activePointerItem = Get-Item -LiteralPath $activePointer -Force -ErrorAction SilentlyContinue
|
|
402
|
+
if ($activePointerItem) { return $null }
|
|
403
|
+
|
|
404
|
+
$legacy = Join-Path (Get-Location) "task_plan.md"
|
|
405
|
+
return (Resolve-ContainedPlanFile -Candidate $legacy -ExpectedDirectory (Get-Location).Path)
|
|
70
406
|
}
|
|
71
407
|
|
|
72
408
|
function Get-AttestationPath {
|
|
@@ -86,18 +422,19 @@ if (-not $planFile) {
|
|
|
86
422
|
}
|
|
87
423
|
|
|
88
424
|
$attestationFile = Get-AttestationPath -PlanFile $planFile
|
|
425
|
+
$attestationDir = Split-Path -Parent $attestationFile
|
|
89
426
|
|
|
90
427
|
if ($Show) {
|
|
91
|
-
if (
|
|
428
|
+
if (Get-Item -LiteralPath $attestationFile -Force -ErrorAction SilentlyContinue) {
|
|
92
429
|
Write-Output "Plan: $planFile"
|
|
93
430
|
Write-Output "Attestation: $attestationFile"
|
|
94
|
-
Write-Output ("SHA-256: " + (
|
|
431
|
+
Write-Output ("SHA-256: " + (Read-SafeText -Path $attestationFile -ExpectedDirectory $attestationDir -MaxBytes 4096).Trim())
|
|
95
432
|
# Nonce (security A1.4): surface the per-plan nonce if init-session
|
|
96
433
|
# generated one next to the attestation. Informational only here; the
|
|
97
434
|
# hooks consume it to build collision-proof BEGIN/END delimiters.
|
|
98
435
|
$nonceFile = Join-Path (Split-Path -Parent $attestationFile) ".nonce"
|
|
99
|
-
if (
|
|
100
|
-
$nonceVal = (
|
|
436
|
+
if (Get-Item -LiteralPath $nonceFile -Force -ErrorAction SilentlyContinue) {
|
|
437
|
+
$nonceVal = (Read-SafeText -Path $nonceFile -ExpectedDirectory $attestationDir -MaxBytes 4096).Trim()
|
|
101
438
|
if ($nonceVal) { Write-Output "Nonce: $nonceVal" }
|
|
102
439
|
}
|
|
103
440
|
} else {
|
|
@@ -108,8 +445,8 @@ if ($Show) {
|
|
|
108
445
|
}
|
|
109
446
|
|
|
110
447
|
if ($Clear) {
|
|
111
|
-
if (
|
|
112
|
-
Remove-
|
|
448
|
+
if (Get-Item -LiteralPath $attestationFile -Force -ErrorAction SilentlyContinue) {
|
|
449
|
+
Remove-SafeFile -Path $attestationFile -ExpectedDirectory $attestationDir
|
|
113
450
|
Write-Output "[plan-attest] Cleared attestation for $planFile."
|
|
114
451
|
} else {
|
|
115
452
|
Write-Output "[plan-attest] No attestation to clear."
|
|
@@ -117,13 +454,23 @@ if ($Clear) {
|
|
|
117
454
|
exit 0
|
|
118
455
|
}
|
|
119
456
|
|
|
120
|
-
$
|
|
121
|
-
|
|
457
|
+
$planStream = Open-SafeReadStream -Path $planFile -ExpectedDirectory (Split-Path -Parent $planFile)
|
|
458
|
+
try {
|
|
459
|
+
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
460
|
+
try {
|
|
461
|
+
$hashBytes = $sha256.ComputeHash($planStream)
|
|
462
|
+
} finally {
|
|
463
|
+
$sha256.Dispose()
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
$planStream.Dispose()
|
|
467
|
+
}
|
|
468
|
+
$hashVal = ([BitConverter]::ToString($hashBytes)).Replace("-", "").ToLowerInvariant()
|
|
469
|
+
$storedHash = Write-SafeAscii -Path $attestationFile -ExpectedDirectory $attestationDir -Value $hashVal
|
|
122
470
|
|
|
123
471
|
# Integrity verification (security A2.1): confirm the on-disk attestation
|
|
124
472
|
# matches the intended hash before reporting success. A silent write failure
|
|
125
473
|
# (permissions, full disk) must not leave a stale attestation and exit clean.
|
|
126
|
-
$storedHash = (Get-Content -LiteralPath $attestationFile -Raw -ErrorAction SilentlyContinue)
|
|
127
474
|
if ($null -ne $storedHash) { $storedHash = $storedHash.Trim() }
|
|
128
475
|
if ($storedHash -ne $hashVal) {
|
|
129
476
|
Write-Error "[plan-attest] Attestation write verification FAILED for $attestationFile. Expected $hashVal, found $storedHash. The plan is NOT attested."
|
package/scripts/init-session.sh
CHANGED
|
@@ -24,6 +24,21 @@
|
|
|
24
24
|
|
|
25
25
|
set -e
|
|
26
26
|
|
|
27
|
+
usage() {
|
|
28
|
+
cat << 'EOF'
|
|
29
|
+
Usage: init-session.sh [OPTIONS] [PROJECT NAME]
|
|
30
|
+
|
|
31
|
+
Initialize task_plan.md, findings.md, and progress.md for a planning session.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-t, --template TYPE Use the default or analytics template.
|
|
35
|
+
--plan-dir Create an isolated plan directory without a name.
|
|
36
|
+
--autonomous Enable autonomous mode and plan attestation.
|
|
37
|
+
--gated Enable autonomous mode with the completion gate.
|
|
38
|
+
-h, --help Print this help and exit without changing files.
|
|
39
|
+
EOF
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
TEMPLATE="default"
|
|
28
43
|
PROJECT_NAME=""
|
|
29
44
|
USE_PLAN_DIR=0
|
|
@@ -51,6 +66,10 @@ while [ $# -gt 0 ]; do
|
|
|
51
66
|
MODE="gated"
|
|
52
67
|
shift
|
|
53
68
|
;;
|
|
69
|
+
--help|-h)
|
|
70
|
+
usage
|
|
71
|
+
exit 0
|
|
72
|
+
;;
|
|
54
73
|
*)
|
|
55
74
|
if [ -z "$PROJECT_NAME" ]; then
|
|
56
75
|
PROJECT_NAME="$1"
|